From 1db9d5b3052116586c6ce06ee745d5594f045bc8 Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 21 Aug 2025 16:44:54 +0800 Subject: [PATCH 01/20] feat(tracing): reconstruct tracing module --- veadk/agent.py | 6 +- veadk/runner.py | 4 +- veadk/tracing/base_tracer.py | 205 +----------------- .../telemetry/attributes/attributes.py | 32 +++ .../common_attributes_extractors.py | 21 ++ .../extractors/llm_attributes_extrators.py | 65 ++++++ .../extractors/tool_attributes_extractors.py | 0 .../telemetry/exporters/apmplus_exporter.py | 58 ++--- .../telemetry/exporters/base_exporter.py | 22 +- .../telemetry/exporters/cozeloop_exporter.py | 28 ++- .../telemetry/exporters/inmemory_exporter.py | 28 ++- .../telemetry/exporters/tls_exporter.py | 26 ++- .../tracing/telemetry/opentelemetry_tracer.py | 121 ++++++----- veadk/tracing/telemetry/telemetry.py | 65 ++++++ veadk/utils/misc.py | 2 +- veadk/utils/patches.py | 25 +++ 16 files changed, 359 insertions(+), 349 deletions(-) create mode 100644 veadk/tracing/telemetry/attributes/attributes.py create mode 100644 veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py create mode 100644 veadk/tracing/telemetry/attributes/extractors/llm_attributes_extrators.py create mode 100644 veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py create mode 100644 veadk/tracing/telemetry/telemetry.py diff --git a/veadk/agent.py b/veadk/agent.py index 8a8f756d..f73eaca4 100644 --- a/veadk/agent.py +++ b/veadk/agent.py @@ -112,9 +112,9 @@ def model_post_init(self, __context: Any) -> None: self.tools.append(load_memory) - if self.tracers: - for tracer in self.tracers: - tracer.do_hooks(self) + # if self.tracers: + # for tracer in self.tracers: + # tracer.do_hooks(self) logger.info(f"{self.__class__.__name__} `{self.name}` init done.") logger.debug( diff --git a/veadk/runner.py b/veadk/runner.py index bee47251..554d8594 100644 --- a/veadk/runner.py +++ b/veadk/runner.py @@ -68,8 +68,8 @@ def __init__( # prevent VeRemoteAgent has no long-term memory attr if isinstance(self.agent, Agent): self.long_term_memory = self.agent.long_term_memory - for tracer in self.agent.tracers: - tracer.set_app_name(self.app_name) + # for tracer in self.agent.tracers: + # tracer.set_app_name(self.app_name) else: self.long_term_memory = None diff --git a/veadk/tracing/base_tracer.py b/veadk/tracing/base_tracer.py index a3021048..8ec9e46b 100644 --- a/veadk/tracing/base_tracer.py +++ b/veadk/tracing/base_tracer.py @@ -12,16 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json from abc import ABC, abstractmethod -from typing import Any, Optional +from typing import Optional -from google.adk.agents.callback_context import CallbackContext from google.adk.agents.invocation_context import InvocationContext -from google.adk.models.llm_request import LlmRequest -from google.adk.models.llm_response import LlmResponse from google.adk.plugins.base_plugin import BasePlugin -from google.adk.tools import BaseTool, ToolContext from google.genai import types from opentelemetry import trace @@ -102,201 +97,9 @@ def replace_bytes_with_empty(data): class BaseTracer(ABC): def __init__(self, name: str): - self.app_name = "veadk_app_name" - pass + self.name = name + self._trace_id = "" + self._trace_file_path = "" @abstractmethod def dump(self, user_id: str, session_id: str, path: str = "/tmp") -> str: ... - - def tracer_hook_before_model( - self, callback_context: CallbackContext, llm_request: LlmRequest - ) -> Optional[LlmResponse]: - """agent run stage""" - trace.get_tracer("gcp.vertex.agent") - span = trace.get_current_span() - # logger.debug(f"llm_request: {llm_request}") - - req = llm_request.model_dump() - - app_name = getattr(self, "app_name", "veadk_app") - agent_name = callback_context.agent_name - model_name = req.get("model", "unknown") - max_tokens = ( - None - if not req.get("live_connect_config") - else req["live_connect_config"].get("max_output_tokens", None) - ) - temperature = ( - None - if not req.get("live_connect_config") - else req["live_connect_config"].get("temperature", None) - ) - top_p = ( - None - if not req.get("live_connect_config") - else req["live_connect_config"].get("top_p", None) - ) - - attributes = {} - attributes["agent.name"] = agent_name - attributes["app.name"] = app_name - attributes["gen_ai.system"] = "veadk" - if model_name: - attributes["gen_ai.request.model"] = model_name - attributes["gen_ai.response.model"] = ( - model_name # The req model and the resp model should be consistent. - ) - attributes["gen_ai.request.type"] = "completion" - if max_tokens: - attributes["gen_ai.request.max_tokens"] = max_tokens - if temperature: - attributes["gen_ai.request.temperature"] = temperature - if top_p: - attributes["gen_ai.request.top_p"] = top_p - - # Print attributes for debugging - # print("Tracing attributes:", attributes) - - # Set all attributes at once if possible, else fallback to individual - if hasattr(span, "set_attributes"): - span.set_attributes(attributes) - else: - # Fallback for OpenTelemetry versions without set_attributes - for k, v in attributes.items(): - span.set_attribute(k, v) - - def tracer_hook_after_model( - self, callback_context: CallbackContext, llm_response: LlmResponse - ) -> Optional[LlmResponse]: - """call llm stage""" - trace.get_tracer("gcp.vertex.agent") - span = trace.get_current_span() - # logger.debug(f"llm_response: {llm_response}") - # logger.debug(f"callback_context: {callback_context}") - - # Refined: collect all attributes, use set_attributes, print for debugging - attributes = {} - - app_name = getattr(self, "app_name", "veadk_app") - agent_name = callback_context.agent_name - attributes["agent.name"] = agent_name - attributes["app.name"] = app_name - attributes["gen_ai.system"] = "veadk" - # prompt - user_content = callback_context.user_content - role = None - content = None - if getattr(user_content, "role", None): - role = getattr(user_content, "role", None) - - if user_content and getattr(user_content, "parts", None): - # content = user_content.model_dump_json(exclude_none=True) - content = user_content.model_dump(exclude_none=True).get("parts", None) - if content: - content = replace_bytes_with_empty(content) - content = json.dumps(content, ensure_ascii=False) if content else None - - if role and content: - attributes["gen_ai.prompt.0.role"] = role - attributes["gen_ai.prompt.0.content"] = content - - # completion - completion_content = getattr(llm_response, "content").model_dump( - exclude_none=True - ) - if completion_content: - content = json.dumps( - getattr(llm_response, "content").model_dump(exclude_none=True)["parts"] - ) - role = getattr(llm_response, "content").model_dump(exclude_none=True)[ - "role" - ] - if role and content: - attributes["gen_ai.completion.0.role"] = role - attributes["gen_ai.completion.0.content"] = content - - if not llm_response.usage_metadata: - return - - # tokens - metadata = llm_response.usage_metadata.model_dump() - if metadata: - prompt_tokens = metadata.get("prompt_token_count", None) - completion_tokens = metadata.get("candidates_token_count", None) - total_tokens = metadata.get("total_token_count", None) - cache_read_input_tokens = ( - metadata.get("cache_read_input_tokens") or 0 - ) # Might change, once openai introduces their equivalent. - cache_create_input_tokens = ( - metadata.get("cache_create_input_tokens") or 0 - ) # Might change, once openai introduces their equivalent. - if prompt_tokens: - attributes["gen_ai.usage.prompt_tokens"] = prompt_tokens - if completion_tokens: - attributes["gen_ai.usage.completion_tokens"] = completion_tokens - if total_tokens: - attributes["gen_ai.usage.total_tokens"] = total_tokens - if cache_read_input_tokens is not None: - attributes["gen_ai.usage.cache_read_input_tokens"] = ( - cache_read_input_tokens - ) - if cache_create_input_tokens is not None: - attributes["gen_ai.usage.cache_create_input_tokens"] = ( - cache_create_input_tokens - ) - - # Print attributes for debugging - # print("Tracing attributes:", attributes) - - # Set all attributes at once if possible, else fallback to individual - if hasattr(span, "set_attributes"): - span.set_attributes(attributes) - else: - # Fallback for OpenTelemetry versions without set_attributes - for k, v in attributes.items(): - span.set_attribute(k, v) - - def tracer_hook_after_tool( - self, - tool: BaseTool, - args: dict[str, Any], - tool_context: ToolContext, - tool_response: dict, - ): - trace.get_tracer("gcp.vertex.agent") - span = trace.get_current_span() - agent_name = tool_context.agent_name - tool_name = tool.name - app_name = getattr(self, "app_name", "veadk_app") - attributes = { - "agent.name": agent_name, - "app.name": app_name, - "tool.name": tool_name, - "gen_ai.system": "veadk", - } - - # Set all attributes at once if possible, else fallback to individual - if hasattr(span, "set_attributes"): - span.set_attributes(attributes) - else: - # Fallback for OpenTelemetry versions without set_attributes - for k, v in attributes.items(): - span.set_attribute(k, v) - - def set_app_name(self, app_name): - self.app_name = app_name - - def do_hooks(self, agent) -> None: - if not getattr(agent, "before_model_callback", None): - agent.before_model_callback = [] - if not getattr(agent, "after_model_callback", None): - agent.after_model_callback = [] - if not getattr(agent, "after_tool_callback", None): - agent.after_tool_callback = [] - - if self.tracer_hook_before_model not in agent.before_model_callback: - agent.before_model_callback.append(self.tracer_hook_before_model) - if self.tracer_hook_after_model not in agent.after_model_callback: - agent.after_model_callback.append(self.tracer_hook_after_model) - if self.tracer_hook_after_tool not in agent.after_tool_callback: - agent.after_tool_callback.append(self.tracer_hook_after_tool) diff --git a/veadk/tracing/telemetry/attributes/attributes.py b/veadk/tracing/telemetry/attributes/attributes.py new file mode 100644 index 00000000..e1f94261 --- /dev/null +++ b/veadk/tracing/telemetry/attributes/attributes.py @@ -0,0 +1,32 @@ +from veadk.tracing.telemetry.attributes.extractors.common_attributes_extractors import ( + common_gen_ai_app_name, + common_gen_ai_session_id, + common_gen_ai_system, + common_gen_ai_system_version, + common_gen_ai_user_id, +) +from veadk.tracing.telemetry.attributes.extractors.llm_attributes_extrators import ( + llm_gen_ai_completion, + llm_gen_ai_prompt, + llm_gen_ai_request_model, + llm_gen_ai_request_type, + llm_gen_ai_response_model, +) + +ATTRIBUTES = { + "common": { + "gen_ai.system": common_gen_ai_system, + "gen_ai.system_version": common_gen_ai_system_version, + "gen_ai.app.name": common_gen_ai_app_name, + "gen_ai.user.id": common_gen_ai_user_id, + "gen_ai.session.id": common_gen_ai_session_id, + }, + "llm": { + "gen_ai.request.model": llm_gen_ai_request_model, + "gen_ai.request.type": llm_gen_ai_request_type, + "gen_ai.response.model": llm_gen_ai_response_model, + "gen_ai.prompt": llm_gen_ai_prompt, + "gen_ai.completion": llm_gen_ai_completion, + }, + "tool": ..., +} diff --git a/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py new file mode 100644 index 00000000..19c62010 --- /dev/null +++ b/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py @@ -0,0 +1,21 @@ +from veadk.version import VERSION + + +def common_gen_ai_system() -> str: + return "veadk" + + +def common_gen_ai_system_version() -> str: + return VERSION + + +def common_gen_ai_app_name(**kwargs) -> str: + return kwargs.get("app_name", "") + + +def common_gen_ai_user_id(**kwargs) -> str: + return kwargs.get("user_id", "") + + +def common_gen_ai_session_id(**kwargs) -> str: + return kwargs.get("session_id", "") diff --git a/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extrators.py b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extrators.py new file mode 100644 index 00000000..ecb0f9d7 --- /dev/null +++ b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extrators.py @@ -0,0 +1,65 @@ +from attr import dataclass +from google.adk.agents.invocation_context import InvocationContext +from google.adk.models.llm_request import LlmRequest +from google.adk.models.llm_response import LlmResponse + + +@dataclass +class LLMAttributesParams: + invocation_context: InvocationContext + event_id: str + llm_request: LlmRequest + llm_response: LlmResponse + + +def llm_gen_ai_request_model(params: LLMAttributesParams) -> str: + return params.llm_request.model or "" + + +def llm_gen_ai_request_type(params: LLMAttributesParams) -> str | list[str]: + type = "completion" + return type or "" + + +def llm_gen_ai_response_model(params: LLMAttributesParams) -> str: + return params.llm_request.model or "" + + +def llm_gen_ai_request_max_tokens(params: LLMAttributesParams) -> int | None: + return params.llm_request.config.max_output_tokens + + +def llm_gen_ai_request_temperature(params: LLMAttributesParams) -> float | None: + return params.llm_request.config.temperature + + +def llm_gen_ai_request_top_p(params: LLMAttributesParams) -> float | None: + return params.llm_request.config.top_p + + +def llm_gen_ai_prompt(params: LLMAttributesParams) -> list[dict]: + ret = [] + for idx, content in enumerate(params.llm_request.contents): + if content.parts: + role = content.role + parts = [part for part in content.parts if not part.inline_data] + ret.append({f".{idx}.role": role, f".{idx}.content": str(parts)}) + return ret + + +def llm_gen_ai_completion(params: LLMAttributesParams) -> list[dict] | None: + ret = [] + + content = params.llm_response.content + if content and content.parts: + parts = [part for part in content.parts if not part.inline_data] + ret.append({f".{0}.role": content.role, f".{0}.content": str(parts)}) + return ret + + +def llm_gen_ai_response_stop_reason(params: LLMAttributesParams) -> str | None: + return params.llm_response.stop_reason + + +def llm_gen_ai_response_finish_reason(params: LLMAttributesParams) -> str | None: + return params.llm_response.finish_reason diff --git a/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py new file mode 100644 index 00000000..e69de29b diff --git a/veadk/tracing/telemetry/exporters/apmplus_exporter.py b/veadk/tracing/telemetry/exporters/apmplus_exporter.py index 2844a6ef..ada704c3 100644 --- a/veadk/tracing/telemetry/exporters/apmplus_exporter.py +++ b/veadk/tracing/telemetry/exporters/apmplus_exporter.py @@ -24,7 +24,6 @@ from veadk.config import getenv from veadk.tracing.telemetry.exporters.base_exporter import BaseExporter -from veadk.tracing.telemetry.metrics.opentelemetry_metrics import MeterContext from veadk.utils.logger import get_logger logger = get_logger(__name__) @@ -52,47 +51,38 @@ class APMPlusExporterConfig(BaseModel): class APMPlusExporter(BaseModel, BaseExporter): config: APMPlusExporterConfig = Field(default_factory=APMPlusExporterConfig) - @override - def get_processor(self): - resource_attributes = { - "service.name": self.config.service_name, - } - + def model_post_init(self) -> None: headers = { "x-byteapm-appkey": self.config.app_key, } - exporter = OTLPSpanExporter( - endpoint=self.config.endpoint, insecure=True, headers=headers - ) - self._real_exporter = exporter - processor = BatchSpanProcessor(exporter) - return processor, resource_attributes - - def export(self): - self._real_exporter.force_flush() - logger.info( - f"APMPlusExporter exports data to {self.config.endpoint}, service name: {self.config.service_name}" - ) + self.headers |= headers - @override - def get_meter_context(self) -> MeterContext: resource_attributes = { "service.name": self.config.service_name, } - endpoint = self.config.endpoint - headers = { - "x-byteapm-appkey": self.config.app_key, - } + self.resource_attributes |= resource_attributes - resource = Resource.create(resource_attributes) - exporter = OTLPMetricExporter(endpoint=endpoint, headers=headers) + self._exporter = OTLPSpanExporter( + endpoint=self.config.endpoint, insecure=True, headers=self.headers + ) + self.processor = BatchSpanProcessor(self._exporter) + + # init meter + resource = Resource.create() + exporter = OTLPMetricExporter(endpoint=self.config.endpoint, headers=headers) metric_reader = PeriodicExportingMetricReader(exporter) provider = MeterProvider(metric_readers=[metric_reader], resource=resource) metrics.set_meter_provider(provider) - meter = metrics.get_meter("my.meter.name") - meter_context = MeterContext( - meter=meter, - provider=provider, - reader=metric_reader, - ) - return meter_context + + # metrics.get_meter("veadk.apmplus.meter") + + @override + def export(self) -> None: + if self._exporter: + self._exporter.force_flush() + + logger.info( + f"APMPlusExporter exports data to {self.config.endpoint}, service name: {self.config.service_name}" + ) + else: + logger.warning("APMPlusExporter internal exporter is not initialized.") diff --git a/veadk/tracing/telemetry/exporters/base_exporter.py b/veadk/tracing/telemetry/exporters/base_exporter.py index a5e819f1..ad74db66 100644 --- a/veadk/tracing/telemetry/exporters/base_exporter.py +++ b/veadk/tracing/telemetry/exporters/base_exporter.py @@ -12,20 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -from abc import ABC, abstractmethod -from typing import Any +from abc import ABC +from opentelemetry.sdk.trace import SpanProcessor +from opentelemetry.sdk.trace.export import SpanExporter -class BaseExporter(ABC): - def __init__(self) -> None: - pass - @abstractmethod - def get_processor(self) -> Any: - pass +class BaseExporter(ABC): + def __init__( + self, resource_attributes: dict | None = None, headers: dict | None = None + ) -> None: + self.resource_attributes = resource_attributes or {} + self.headers = headers or {} - def get_meter_context(self) -> Any: - pass + self._exporter: SpanExporter | None = None + self.processor: SpanProcessor | None = None def export(self) -> None: + """Force export of telemetry data.""" pass diff --git a/veadk/tracing/telemetry/exporters/cozeloop_exporter.py b/veadk/tracing/telemetry/exporters/cozeloop_exporter.py index 624808ae..d321b89e 100644 --- a/veadk/tracing/telemetry/exporters/cozeloop_exporter.py +++ b/veadk/tracing/telemetry/exporters/cozeloop_exporter.py @@ -44,23 +44,27 @@ class CozeloopExporterConfig(BaseModel): class CozeloopExporter(BaseModel, BaseExporter): config: CozeloopExporterConfig = Field(default_factory=CozeloopExporterConfig) - @override - def get_processor(self): + def model_post_init(self) -> None: headers = { "cozeloop-workspace-id": self.config.space_id, - "authorization": f"Bearer {self.config.token}", } - exporter = OTLPSpanExporter( + self.headers |= headers + + self._exporter = OTLPSpanExporter( endpoint=self.config.endpoint, headers=headers, timeout=10, ) - self._real_exporter = exporter - processor = BatchSpanProcessor(exporter) - return processor, None - def export(self): - self._real_exporter.force_flush() - logger.info( - f"CozeloopExporter exports data to {self.config.endpoint}, space id: {self.config.space_id}" - ) + self.processor = BatchSpanProcessor(self._exporter) + + @override + def export(self) -> None: + """Force export of telemetry data.""" + if self._exporter: + self._exporter.force_flush() + logger.info( + f"CozeloopExporter exports data to {self.config.endpoint}, space id: {self.config.space_id}" + ) + else: + logger.warning("CozeloopExporter internal exporter is not initialized.") diff --git a/veadk/tracing/telemetry/exporters/inmemory_exporter.py b/veadk/tracing/telemetry/exporters/inmemory_exporter.py index 8647f580..971c6448 100644 --- a/veadk/tracing/telemetry/exporters/inmemory_exporter.py +++ b/veadk/tracing/telemetry/exporters/inmemory_exporter.py @@ -12,11 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -import typing -from typing import Any +from typing import Sequence from opentelemetry.sdk.trace import ReadableSpan, export -from pydantic import BaseModel from typing_extensions import override from veadk.tracing.telemetry.exporters.base_exporter import BaseExporter @@ -27,16 +25,16 @@ # ======== Adapted from Google ADK ======== class _InMemoryExporter(export.SpanExporter): - def __init__(self, session_trace_dict): + def __init__(self): super().__init__() self._spans = [] - self.session_trace_dict = session_trace_dict + self.session_trace_dict = {} self.trace_id = "" self.prompt_tokens = [] self.completion_tokens = [] @override - def export(self, spans: typing.Sequence[ReadableSpan]) -> export.SpanExportResult: + def export(self, spans: Sequence[ReadableSpan]) -> export.SpanExportResult: for span in spans: if span.context: trace_id = span.context.trace_id @@ -56,6 +54,7 @@ def export(self, spans: typing.Sequence[ReadableSpan]) -> export.SpanExportResul self.prompt_tokens.append(prompt_token) if completion_token: self.completion_tokens.append(completion_token) + if span.name == "call_llm": attributes = dict(span.attributes or {}) session_id = attributes.get("gcp.vertex.agent.session_id", None) @@ -81,14 +80,13 @@ def clear(self): self._spans.clear() -class InMemoryExporter(BaseModel, BaseExporter): - name: str = "inmemory_exporter" +class InMemoryExporter(BaseExporter): + """InMemory Exporter mainly for store spans in memory for debugging / observability purposes.""" + + def __init__(self, name: str = "inmemory_exporter") -> None: + super().__init__() - def model_post_init(self, __context: Any) -> None: - self._exporter = _InMemoryExporter({}) + self.name = name - @override - def get_processor(self): - self._real_exporter = self._exporter - processor = export.SimpleSpanProcessor(self._exporter) - return processor, None + self._exporter = _InMemoryExporter() + self.processor = export.SimpleSpanProcessor(self._exporter) diff --git a/veadk/tracing/telemetry/exporters/tls_exporter.py b/veadk/tracing/telemetry/exporters/tls_exporter.py index 634d6df2..927072a7 100644 --- a/veadk/tracing/telemetry/exporters/tls_exporter.py +++ b/veadk/tracing/telemetry/exporters/tls_exporter.py @@ -47,25 +47,29 @@ class TLSExporterConfig(BaseModel): class TLSExporter(BaseModel, BaseExporter): config: TLSExporterConfig = Field(default_factory=TLSExporterConfig) - @override - def get_processor(self): + def model_post_init(self) -> None: headers = { "x-tls-otel-tracetopic": self.config.topic_id, "x-tls-otel-ak": self.config.access_key, "x-tls-otel-sk": self.config.secret_key, "x-tls-otel-region": self.config.region, } - exporter = OTLPSpanExporter( + self.headers |= headers + + self._exporter = OTLPSpanExporter( endpoint=self.config.endpoint, headers=headers, timeout=10, ) - self._real_exporter = exporter - processor = BatchSpanProcessor(exporter) - return processor, None - def export(self): - self._real_exporter.force_flush() - logger.info( - f"TLSExporter exports data to {self.config.endpoint}, topic id: {self.config.topic_id}" - ) + self.processor = BatchSpanProcessor(self._exporter) + + @override + def export(self) -> None: + if self._exporter: + self._exporter.force_flush() + logger.info( + f"TLSExporter exports data to {self.config.endpoint}, topic id: {self.config.topic_id}" + ) + else: + logger.warning("TLSExporter internal exporter is not initialized.") diff --git a/veadk/tracing/telemetry/opentelemetry_tracer.py b/veadk/tracing/telemetry/opentelemetry_tracer.py index 4159df75..b70d266b 100644 --- a/veadk/tracing/telemetry/opentelemetry_tracer.py +++ b/veadk/tracing/telemetry/opentelemetry_tracer.py @@ -25,15 +25,15 @@ from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, field_validator from typing_extensions import override from veadk.tracing.base_tracer import BaseTracer from veadk.tracing.telemetry.exporters.apmplus_exporter import APMPlusExporter from veadk.tracing.telemetry.exporters.base_exporter import BaseExporter from veadk.tracing.telemetry.exporters.inmemory_exporter import InMemoryExporter -from veadk.tracing.telemetry.metrics.opentelemetry_metrics import MeterUploader from veadk.utils.logger import get_logger +from veadk.utils.patches import patch_google_adk_telemetry logger = get_logger(__name__) @@ -46,10 +46,7 @@ def update_resource_attributions(provider: TracerProvider, resource_attributes: class OpentelemetryTracer(BaseModel, BaseTracer): model_config = ConfigDict(arbitrary_types_allowed=True) - exporters: list[BaseExporter] = Field( - default=[], - description="The exporters to export spans.", - ) + name: str = Field( default=DEFAULT_VEADK_TRACER_NAME, description="The identifier of tracer." ) @@ -59,55 +56,44 @@ class OpentelemetryTracer(BaseModel, BaseTracer): description="The identifier of app.", ) - def model_post_init(self, context: Any, /) -> None: - self._processors = [] - self._inmemory_exporter: InMemoryExporter | None = None - - # InMemoryExporter is a default exporter for exporting local tracing file - for exporter in self.exporters: - if isinstance(exporter, InMemoryExporter): - self._inmemory_exporter = exporter - - if self._inmemory_exporter is None: - self._inmemory_exporter = InMemoryExporter() - self.exporters.append(self._inmemory_exporter) - # ======================================================================== + exporters: list[BaseExporter] = Field( + default_factory=list, + description="The exporters to export spans.", + ) - # Process meter-related attributes - self._meter_contexts = [] - self._meter_uploaders = [] - for exporter in self.exporters: - meter_context = exporter.get_meter_context() - if meter_context is not None: - self._meter_contexts.append(meter_context) + @field_validator("exporters") + @classmethod + def forbid_inmemory_exporter(cls, v: list[BaseExporter]) -> list[BaseExporter]: + for e in v: + if isinstance(e, InMemoryExporter): + raise ValueError("InMemoryExporter is not allowed in exporters list") + return v - for meter_context in self._meter_contexts: - meter_uploader = MeterUploader(meter_context) - self._meter_uploaders.append(meter_uploader) - # ================================ + def model_post_init(self, context: Any) -> None: + patch_google_adk_telemetry() - # init tracer provider - # VeADK operates on global OpenTelemetry provider, hence return nothing - self._init_tracer_provider() + self._processors = [] + self._inmemory_exporter = InMemoryExporter() + self.exporters.append(self._inmemory_exporter) - # just for debug - self._trace_file_path = "" + # VeADK operates on global OpenTelemetry provider, return nothing + self._init_global_tracer_provider() GoogleADKInstrumentor().instrument() - def _init_tracer_provider(self) -> None: - # set provider anyway - # finally, get global provider + def _init_global_tracer_provider(self) -> None: + # set provider anyway, then get global provider tracer_provider = trace_sdk.TracerProvider() trace_api.set_tracer_provider(tracer_provider) global_tracer_provider: TracerProvider = trace_api.get_tracer_provider() # type: ignore - have_apmplus_exporter = False - for processor in global_tracer_provider._active_span_processor._span_processors: - if isinstance(processor, (BatchSpanProcessor, SimpleSpanProcessor)): - if isinstance(processor.span_exporter, OTLPSpanExporter): - if "apmplus" in processor.span_exporter._endpoint: - have_apmplus_exporter = True + span_processors = global_tracer_provider._active_span_processor._span_processors + have_apmplus_exporter = any( + isinstance(p, (BatchSpanProcessor, SimpleSpanProcessor)) + and isinstance(p.span_exporter, OTLPSpanExporter) + and "apmplus" in p.span_exporter._endpoint + for p in span_processors + ) if have_apmplus_exporter: self.exporters = [ @@ -115,27 +101,42 @@ def _init_tracer_provider(self) -> None: ] for exporter in self.exporters: - processor, resource_attributes = exporter.get_processor() - if resource_attributes is not None: + processor = exporter.processor + resource_attributes = exporter.resource_attributes + + if resource_attributes: update_resource_attributions( global_tracer_provider, resource_attributes ) - global_tracer_provider.add_span_processor(processor) - logger.debug( - f"Add exporter `{exporter.__class__.__name__}` to OpentelemetryTracer." - ) - self._processors.append(processor) - logger.debug(f"Init OpentelemetryTracer with {len(self.exporters)} exporters.") - def get_trace_id(self) -> str: - if not self._inmemory_exporter: - return "" - try: - trace_id = hex(int(self._inmemory_exporter._real_exporter.trace_id))[2:] - except Exception: - return "" + if processor: + global_tracer_provider.add_span_processor(processor) + self._processors.append(processor) - return trace_id + logger.debug( + f"Add span processor for exporter `{exporter.__class__.__name__}` to OpentelemetryTracer." + ) + else: + logger.error( + f"Add span processor for exporter `{exporter.__class__.__name__}` to OpentelemetryTracer failed." + ) + + logger.debug( + f"Init OpentelemetryTracer with {len(self._processors)} exporters." + ) + + @property + def trace_file_path(self) -> str: + return self._trace_file_path + + @property + def trace_id(self) -> str: + try: + trace_id = hex(int(self._inmemory_exporter._exporter.trace_id))[2:] + return trace_id + except Exception as e: + logger.error(f"Failed to get trace_id from InMemoryExporter: {e}") + return self._trace_id @override def dump( diff --git a/veadk/tracing/telemetry/telemetry.py b/veadk/tracing/telemetry/telemetry.py new file mode 100644 index 00000000..6ef6d20d --- /dev/null +++ b/veadk/tracing/telemetry/telemetry.py @@ -0,0 +1,65 @@ +from typing import Any + +from google.adk.agents.invocation_context import InvocationContext +from google.adk.events import Event +from google.adk.models.llm_request import LlmRequest +from google.adk.models.llm_response import LlmResponse +from google.adk.tools import BaseTool +from google.genai import types +from opentelemetry import trace + +from veadk.tracing.telemetry.attributes.attributes import ATTRIBUTES +from veadk.utils.logger import get_logger + +logger = get_logger(__name__) + + +def _set_common_attributes(): + pass + + +def trace_send_data( + invocation_context: InvocationContext, + event_id: str, + data: list[types.Content], +) -> None: ... + + +def trace_tool_call( + tool: BaseTool, + args: dict[str, Any], + function_response_event: Event, +) -> None: + print("tool_call") + + +def trace_call_llm( + invocation_context: InvocationContext, + event_id: str, + llm_request: LlmRequest, + llm_response: LlmResponse, +) -> None: + span = trace.get_current_span() + + # common_attributes = ATTRIBUTES.get("common", {}) + llm_attributes = ATTRIBUTES.get("llm", {}) + + # for attr_name, attr_extractor in common_attributes.items(): + # # set attribute anyway + # span.set_attribute( + # attr_name, + # attr_extractor(invocation_context, event_id, llm_request, llm_response), + # ) + + for attr_name, attr_extractor in llm_attributes.items(): + # set attribute anyway + value = attr_extractor(invocation_context, event_id, llm_request, llm_response) + if isinstance(value, dict): + for key, val in value.items(): + # gen_ai. and gen_ai_ + span.set_attribute(f"{attr_name}{key}", val) + else: + span.set_attribute( + attr_name, + attr_extractor(invocation_context, event_id, llm_request, llm_response), + ) diff --git a/veadk/utils/misc.py b/veadk/utils/misc.py index 15852522..b10b55b2 100644 --- a/veadk/utils/misc.py +++ b/veadk/utils/misc.py @@ -27,7 +27,7 @@ def read_file(file_path): return data -def formatted_timestamp(): +def formatted_timestamp() -> str: # YYYYMMDDHHMMSS return time.strftime("%Y%m%d%H%M%S", time.localtime()) diff --git a/veadk/utils/patches.py b/veadk/utils/patches.py index 53f472de..8df5c3a6 100644 --- a/veadk/utils/patches.py +++ b/veadk/utils/patches.py @@ -13,7 +13,14 @@ # limitations under the License. import asyncio +import sys +from typing import Callable +from veadk.tracing.telemetry.telemetry import ( + trace_call_llm, + trace_send_data, + trace_tool_call, +) from veadk.utils.logger import get_logger logger = get_logger(__name__) @@ -53,3 +60,21 @@ def patched_cancel_scope_exit(self, exc_type, exc_val, exc_tb): raise CancelScope.__exit__ = patched_cancel_scope_exit + + +def patch_google_adk_telemetry() -> None: + trace_functions = { + "trace_tool_call": trace_tool_call, + "trace_call_llm": trace_call_llm, + "trace_send_data": trace_send_data, + } + + for mod_name, mod in sys.modules.items(): + if mod_name.startswith("google.adk"): + for var_name in dir(mod): + var = getattr(mod, var_name, None) + if var_name in trace_functions.keys() and isinstance(var, Callable): + setattr(mod, var_name, trace_functions[var_name]) + logger.debug( + f"Patch {mod_name} {var_name} with {trace_functions[var_name]}" + ) From b8b81f103d67d756d27757e27d48fc0ef2c42c5e Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 21 Aug 2025 18:01:55 +0800 Subject: [PATCH 02/20] update attributions --- .../telemetry/attributes/attributes.py | 14 +--- .../extractors/llm_attributes_extrators.py | 64 ++++++++++++++++++- .../telemetry/exporters/apmplus_exporter.py | 6 +- .../telemetry/exporters/base_exporter.py | 18 +++--- .../telemetry/exporters/cozeloop_exporter.py | 2 +- .../telemetry/exporters/tls_exporter.py | 2 +- veadk/tracing/telemetry/telemetry.py | 31 ++++----- 7 files changed, 92 insertions(+), 45 deletions(-) diff --git a/veadk/tracing/telemetry/attributes/attributes.py b/veadk/tracing/telemetry/attributes/attributes.py index e1f94261..4aaa9411 100644 --- a/veadk/tracing/telemetry/attributes/attributes.py +++ b/veadk/tracing/telemetry/attributes/attributes.py @@ -6,11 +6,7 @@ common_gen_ai_user_id, ) from veadk.tracing.telemetry.attributes.extractors.llm_attributes_extrators import ( - llm_gen_ai_completion, - llm_gen_ai_prompt, - llm_gen_ai_request_model, - llm_gen_ai_request_type, - llm_gen_ai_response_model, + LLM_ATTRIBUTES, ) ATTRIBUTES = { @@ -21,12 +17,6 @@ "gen_ai.user.id": common_gen_ai_user_id, "gen_ai.session.id": common_gen_ai_session_id, }, - "llm": { - "gen_ai.request.model": llm_gen_ai_request_model, - "gen_ai.request.type": llm_gen_ai_request_type, - "gen_ai.response.model": llm_gen_ai_response_model, - "gen_ai.prompt": llm_gen_ai_prompt, - "gen_ai.completion": llm_gen_ai_completion, - }, + "llm": LLM_ATTRIBUTES, "tool": ..., } diff --git a/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extrators.py b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extrators.py index ecb0f9d7..79d7e9dc 100644 --- a/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extrators.py +++ b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extrators.py @@ -58,8 +58,68 @@ def llm_gen_ai_completion(params: LLMAttributesParams) -> list[dict] | None: def llm_gen_ai_response_stop_reason(params: LLMAttributesParams) -> str | None: - return params.llm_response.stop_reason + return "" def llm_gen_ai_response_finish_reason(params: LLMAttributesParams) -> str | None: - return params.llm_response.finish_reason + # TODO: update to google-adk v1.12.0 + return None + + +def llm_gen_ai_usage_input_tokens(params: LLMAttributesParams) -> int | None: + if params.llm_response.usage_metadata: + return params.llm_response.usage_metadata.prompt_token_count + return None + + +def llm_gen_ai_usage_output_tokens(params: LLMAttributesParams) -> int | None: + if params.llm_response.usage_metadata: + return params.llm_response.usage_metadata.candidates_token_count + return None + + +def llm_gen_ai_usage_total_tokens(params: LLMAttributesParams) -> int | None: + if params.llm_response.usage_metadata: + return params.llm_response.usage_metadata.total_token_count + return None + + +def llm_gen_ai_usage_cache_creation_input_tokens( + params: LLMAttributesParams, +) -> int | None: + if params.llm_response.usage_metadata: + return None + # return params.llm_response.usage_metadata.cached_content_token_count + return None + + +def llm_gen_ai_usage_cache_read_input_tokens(params: LLMAttributesParams) -> int | None: + if params.llm_response.usage_metadata: + return None + # return params.llm_response.usage_metadata.prompt_token_count + return None + + +def llm_gen_ai_is_streaming(params: LLMAttributesParams) -> bool | None: + # return params.llm_request.stream + return None + + +LLM_ATTRIBUTES = { + "gen_ai.request.model": llm_gen_ai_request_model, + "gen_ai.request.type": llm_gen_ai_request_type, + "gen_ai.response.model": llm_gen_ai_response_model, + "gen_ai.request.max_tokens": llm_gen_ai_request_max_tokens, + "gen_ai.request.temperature": llm_gen_ai_request_temperature, + "gen_ai.request.top_p": llm_gen_ai_request_top_p, + "gen_ai.prompt": llm_gen_ai_prompt, + "gen_ai.completion": llm_gen_ai_completion, + "gen_ai.response.stop_reason": llm_gen_ai_response_stop_reason, + "gen_ai.response.finish_reason": llm_gen_ai_response_finish_reason, + "gen_ai.usage.input_tokens": llm_gen_ai_usage_input_tokens, + "gen_ai.usage.output_tokens": llm_gen_ai_usage_output_tokens, + "gen_ai.usage.total_tokens": llm_gen_ai_usage_total_tokens, + "gen_ai.usage.cache_creation_input_tokens": llm_gen_ai_usage_cache_creation_input_tokens, + "gen_ai.usage.cache_read_input_tokens": llm_gen_ai_usage_cache_read_input_tokens, + "gen_ai.is_streaming": llm_gen_ai_is_streaming, +} diff --git a/veadk/tracing/telemetry/exporters/apmplus_exporter.py b/veadk/tracing/telemetry/exporters/apmplus_exporter.py index ada704c3..76d44c19 100644 --- a/veadk/tracing/telemetry/exporters/apmplus_exporter.py +++ b/veadk/tracing/telemetry/exporters/apmplus_exporter.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any + from opentelemetry import metrics from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter @@ -48,10 +50,10 @@ class APMPlusExporterConfig(BaseModel): ) -class APMPlusExporter(BaseModel, BaseExporter): +class APMPlusExporter(BaseExporter): config: APMPlusExporterConfig = Field(default_factory=APMPlusExporterConfig) - def model_post_init(self) -> None: + def model_post_init(self, context: Any) -> None: headers = { "x-byteapm-appkey": self.config.app_key, } diff --git a/veadk/tracing/telemetry/exporters/base_exporter.py b/veadk/tracing/telemetry/exporters/base_exporter.py index ad74db66..7de85965 100644 --- a/veadk/tracing/telemetry/exporters/base_exporter.py +++ b/veadk/tracing/telemetry/exporters/base_exporter.py @@ -12,21 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -from abc import ABC - from opentelemetry.sdk.trace import SpanProcessor from opentelemetry.sdk.trace.export import SpanExporter +from pydantic import BaseModel, ConfigDict, Field + +class BaseExporter(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow") -class BaseExporter(ABC): - def __init__( - self, resource_attributes: dict | None = None, headers: dict | None = None - ) -> None: - self.resource_attributes = resource_attributes or {} - self.headers = headers or {} + resource_attributes: dict = Field(default_factory=dict) + headers: dict = Field(default_factory=dict) - self._exporter: SpanExporter | None = None - self.processor: SpanProcessor | None = None + _exporter: SpanExporter | None = None + processor: SpanProcessor | None = None def export(self) -> None: """Force export of telemetry data.""" diff --git a/veadk/tracing/telemetry/exporters/cozeloop_exporter.py b/veadk/tracing/telemetry/exporters/cozeloop_exporter.py index d321b89e..dd2bc14c 100644 --- a/veadk/tracing/telemetry/exporters/cozeloop_exporter.py +++ b/veadk/tracing/telemetry/exporters/cozeloop_exporter.py @@ -41,7 +41,7 @@ class CozeloopExporterConfig(BaseModel): ) -class CozeloopExporter(BaseModel, BaseExporter): +class CozeloopExporter(BaseExporter): config: CozeloopExporterConfig = Field(default_factory=CozeloopExporterConfig) def model_post_init(self) -> None: diff --git a/veadk/tracing/telemetry/exporters/tls_exporter.py b/veadk/tracing/telemetry/exporters/tls_exporter.py index 927072a7..ba85b700 100644 --- a/veadk/tracing/telemetry/exporters/tls_exporter.py +++ b/veadk/tracing/telemetry/exporters/tls_exporter.py @@ -44,7 +44,7 @@ class TLSExporterConfig(BaseModel): secret_key: str = Field(default_factory=lambda: getenv("VOLCENGINE_SECRET_KEY")) -class TLSExporter(BaseModel, BaseExporter): +class TLSExporter(BaseExporter): config: TLSExporterConfig = Field(default_factory=TLSExporterConfig) def model_post_init(self) -> None: diff --git a/veadk/tracing/telemetry/telemetry.py b/veadk/tracing/telemetry/telemetry.py index 6ef6d20d..494f215e 100644 --- a/veadk/tracing/telemetry/telemetry.py +++ b/veadk/tracing/telemetry/telemetry.py @@ -9,6 +9,9 @@ from opentelemetry import trace from veadk.tracing.telemetry.attributes.attributes import ATTRIBUTES +from veadk.tracing.telemetry.attributes.extractors.llm_attributes_extrators import ( + LLMAttributesParams, +) from veadk.utils.logger import get_logger logger = get_logger(__name__) @@ -41,25 +44,19 @@ def trace_call_llm( ) -> None: span = trace.get_current_span() - # common_attributes = ATTRIBUTES.get("common", {}) llm_attributes = ATTRIBUTES.get("llm", {}) - # for attr_name, attr_extractor in common_attributes.items(): - # # set attribute anyway - # span.set_attribute( - # attr_name, - # attr_extractor(invocation_context, event_id, llm_request, llm_response), - # ) - for attr_name, attr_extractor in llm_attributes.items(): + params = LLMAttributesParams( + invocation_context, event_id, llm_request, llm_response + ) # set attribute anyway - value = attr_extractor(invocation_context, event_id, llm_request, llm_response) - if isinstance(value, dict): - for key, val in value.items(): - # gen_ai. and gen_ai_ - span.set_attribute(f"{attr_name}{key}", val) + value = attr_extractor(params) + if isinstance(value, list): + for _value in value: + for key, val in _value.items(): + # gen_ai. and gen_ai_ + logger.debug(f"Set attribute {attr_name}{key} = {val}") + span.set_attribute(f"{attr_name}{key}", val) else: - span.set_attribute( - attr_name, - attr_extractor(invocation_context, event_id, llm_request, llm_response), - ) + span.set_attribute(attr_name, value) From 2d2aa3c9cc48d9212abe56af813b16252d9306cc Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Thu, 21 Aug 2025 22:23:01 +0800 Subject: [PATCH 03/20] fix exporter bugs --- veadk/runner.py | 14 +---- veadk/tracing/base_tracer.py | 56 ------------------- .../telemetry/attributes/attributes.py | 21 +++---- .../common_attributes_extractors.py | 22 ++++++-- ...rators.py => llm_attributes_extractors.py} | 0 .../extractors/tool_attributes_extractors.py | 15 +++++ .../telemetry/exporters/cozeloop_exporter.py | 11 +++- .../telemetry/exporters/inmemory_exporter.py | 36 ++++-------- .../tracing/telemetry/opentelemetry_tracer.py | 27 ++------- veadk/tracing/telemetry/telemetry.py | 48 ++++++++++++---- 10 files changed, 101 insertions(+), 149 deletions(-) rename veadk/tracing/telemetry/attributes/extractors/{llm_attributes_extrators.py => llm_attributes_extractors.py} (100%) diff --git a/veadk/runner.py b/veadk/runner.py index 554d8594..443b2e4f 100644 --- a/veadk/runner.py +++ b/veadk/runner.py @@ -27,7 +27,6 @@ from veadk.agents.sequential_agent import SequentialAgent from veadk.evaluation import EvalSetRecorder from veadk.memory.short_term_memory import ShortTermMemory -from veadk.tracing.base_tracer import UserMessagePlugin from veadk.types import MediaMessage from veadk.utils.logger import get_logger from veadk.utils.misc import read_png_to_bytes @@ -73,17 +72,6 @@ def __init__( else: self.long_term_memory = None - # process plugins - try: - # try to detect tracer - _ = self.agent.tracers[0] - if not plugins: - plugins = [UserMessagePlugin(name="user_message_plugin")] - else: - plugins.append(UserMessagePlugin(name="user_message_plugin")) - except Exception: - logger.debug("Agent has no tracers, telemetry plugin not added.") - self.runner = ADKRunner( app_name=self.app_name, agent=self.agent, @@ -201,7 +189,7 @@ def _print_trace_id(self): return try: - trace_id = self.agent.tracers[0].get_trace_id() # type: ignore + trace_id = self.agent.tracers[0].trace_id # type: ignore logger.info(f"Trace id: {trace_id}") except Exception as e: logger.warning(f"Get tracer id failed as {e}") diff --git a/veadk/tracing/base_tracer.py b/veadk/tracing/base_tracer.py index 8ec9e46b..bd88bd5a 100644 --- a/veadk/tracing/base_tracer.py +++ b/veadk/tracing/base_tracer.py @@ -13,69 +13,13 @@ # limitations under the License. from abc import ABC, abstractmethod -from typing import Optional -from google.adk.agents.invocation_context import InvocationContext -from google.adk.plugins.base_plugin import BasePlugin -from google.genai import types -from opentelemetry import trace from veadk.utils.logger import get_logger logger = get_logger(__name__) -class UserMessagePlugin(BasePlugin): - def __init__(self, name: str): - super().__init__(name) - - async def on_user_message_callback( - self, - *, - invocation_context: InvocationContext, - user_message: types.Content, - ) -> Optional[types.Content]: - """Callback executed when a user message is received before an invocation starts. - - This callback helps logging and modifying the user message before the - runner starts the invocation. - - Args: - invocation_context: The context for the entire invocation. - user_message: The message content input by user. - - Returns: - An optional `types.Content` to be returned to the ADK. Returning a - value to replace the user message. Returning `None` to proceed - normally. - """ - trace.get_tracer("gcp.vertex.agent") - span = trace.get_current_span() - - logger.debug(f"User message plugin works, catch {span}") - span_name = getattr(span, "name", None) - if span_name and span_name.startswith("invocation"): - agent_name = invocation_context.agent.name - invoke_branch = ( - invocation_context.branch if invocation_context.branch else agent_name - ) - current_session = invocation_context.session - - span.set_attribute("app.name", current_session.app_name) - span.set_attribute("user.id", current_session.user_id) - span.set_attribute("session.id", current_session.id) - - span.set_attribute("agent.name", agent_name) - span.set_attribute("invoke.branch", invoke_branch) - span.set_attribute("gen_ai.system", "veadk") - - logger.debug( - f"Add attributes to {span_name}: app_name={current_session.app_name}, user_id={current_session.user_id}, session_id={current_session.id}, agent_name={agent_name}, invoke_branch={invoke_branch}" - ) - - return None - - def replace_bytes_with_empty(data): """ Recursively traverse the data structure and replace all bytes types with empty strings. diff --git a/veadk/tracing/telemetry/attributes/attributes.py b/veadk/tracing/telemetry/attributes/attributes.py index 4aaa9411..68d82ded 100644 --- a/veadk/tracing/telemetry/attributes/attributes.py +++ b/veadk/tracing/telemetry/attributes/attributes.py @@ -1,22 +1,15 @@ from veadk.tracing.telemetry.attributes.extractors.common_attributes_extractors import ( - common_gen_ai_app_name, - common_gen_ai_session_id, - common_gen_ai_system, - common_gen_ai_system_version, - common_gen_ai_user_id, + COMMON_ATTRIBUTES, ) -from veadk.tracing.telemetry.attributes.extractors.llm_attributes_extrators import ( +from veadk.tracing.telemetry.attributes.extractors.llm_attributes_extractors import ( LLM_ATTRIBUTES, ) +from veadk.tracing.telemetry.attributes.extractors.tool_attributes_extractors import ( + TOOL_ATTRIBUTES, +) ATTRIBUTES = { - "common": { - "gen_ai.system": common_gen_ai_system, - "gen_ai.system_version": common_gen_ai_system_version, - "gen_ai.app.name": common_gen_ai_app_name, - "gen_ai.user.id": common_gen_ai_user_id, - "gen_ai.session.id": common_gen_ai_session_id, - }, + "common": COMMON_ATTRIBUTES, "llm": LLM_ATTRIBUTES, - "tool": ..., + "tool": TOOL_ATTRIBUTES, } diff --git a/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py index 19c62010..5d5d6aab 100644 --- a/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py @@ -1,21 +1,33 @@ from veadk.version import VERSION -def common_gen_ai_system() -> str: +def common_gen_ai_system(**kwargs) -> str: return "veadk" -def common_gen_ai_system_version() -> str: +def common_gen_ai_system_version(**kwargs) -> str: return VERSION def common_gen_ai_app_name(**kwargs) -> str: - return kwargs.get("app_name", "") + app_name = kwargs.get("app_name") + return app_name or "" def common_gen_ai_user_id(**kwargs) -> str: - return kwargs.get("user_id", "") + user_id = kwargs.get("user_id") + return user_id or "" def common_gen_ai_session_id(**kwargs) -> str: - return kwargs.get("session_id", "") + session_id = kwargs.get("session_id") + return session_id or "" + + +COMMON_ATTRIBUTES = { + "gen_ai.system": common_gen_ai_system, + "gen_ai.system.version": common_gen_ai_system_version, + "gen_ai.app.name": common_gen_ai_app_name, + "gen_ai.user.id": common_gen_ai_user_id, + "gen_ai.session.id": common_gen_ai_session_id, +} diff --git a/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extrators.py b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py similarity index 100% rename from veadk/tracing/telemetry/attributes/extractors/llm_attributes_extrators.py rename to veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py diff --git a/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py index e69de29b..b68a9c49 100644 --- a/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py @@ -0,0 +1,15 @@ +from typing import Any + +from attr import dataclass +from google.adk.events import Event +from google.adk.tools import BaseTool + + +@dataclass +class ToolAttributesParams: + tool: BaseTool + args: dict[str, Any] + function_response_event: Event + + +TOOL_ATTRIBUTES = {} diff --git a/veadk/tracing/telemetry/exporters/cozeloop_exporter.py b/veadk/tracing/telemetry/exporters/cozeloop_exporter.py index dd2bc14c..beb6c7ae 100644 --- a/veadk/tracing/telemetry/exporters/cozeloop_exporter.py +++ b/veadk/tracing/telemetry/exporters/cozeloop_exporter.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any + from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.trace.export import BatchSpanProcessor from pydantic import BaseModel, Field @@ -41,18 +43,23 @@ class CozeloopExporterConfig(BaseModel): ) +class _CozeloopExporter(OTLPSpanExporter): + pass + + class CozeloopExporter(BaseExporter): config: CozeloopExporterConfig = Field(default_factory=CozeloopExporterConfig) - def model_post_init(self) -> None: + def model_post_init(self, context: Any) -> None: headers = { "cozeloop-workspace-id": self.config.space_id, + "authorization": f"Bearer {self.config.token}", } self.headers |= headers self._exporter = OTLPSpanExporter( endpoint=self.config.endpoint, - headers=headers, + headers=self.headers, timeout=10, ) diff --git a/veadk/tracing/telemetry/exporters/inmemory_exporter.py b/veadk/tracing/telemetry/exporters/inmemory_exporter.py index 971c6448..af8fff42 100644 --- a/veadk/tracing/telemetry/exporters/inmemory_exporter.py +++ b/veadk/tracing/telemetry/exporters/inmemory_exporter.py @@ -23,46 +23,30 @@ logger = get_logger(__name__) +in_memory_exporter_instance = None + + # ======== Adapted from Google ADK ======== class _InMemoryExporter(export.SpanExporter): - def __init__(self): + def __init__(self) -> None: super().__init__() self._spans = [] - self.session_trace_dict = {} self.trace_id = "" - self.prompt_tokens = [] - self.completion_tokens = [] + self.span_dict = {} + self.session_trace_dict = {} @override def export(self, spans: Sequence[ReadableSpan]) -> export.SpanExportResult: for span in spans: if span.context: - trace_id = span.context.trace_id - self.trace_id = trace_id + self.trace_id = span.context.trace_id + + span_id = span.context.span_id + self.span_dict[span_id] = span else: logger.warning( f"Span context is missing, failed to get `trace_id`. span: {span}" ) - - if span.name == "call_llm": - attributes = dict(span.attributes or {}) - prompt_token = attributes.get("gen_ai.usage.prompt_tokens", None) - completion_token = attributes.get( - "gen_ai.usage.completion_tokens", None - ) - if prompt_token: - self.prompt_tokens.append(prompt_token) - if completion_token: - self.completion_tokens.append(completion_token) - - if span.name == "call_llm": - attributes = dict(span.attributes or {}) - session_id = attributes.get("gcp.vertex.agent.session_id", None) - if session_id: - if session_id not in self.session_trace_dict: - self.session_trace_dict[session_id] = [trace_id] - else: - self.session_trace_dict[session_id] += [trace_id] self._spans.extend(spans) return export.SpanExportResult.SUCCESS diff --git a/veadk/tracing/telemetry/opentelemetry_tracer.py b/veadk/tracing/telemetry/opentelemetry_tracer.py index b70d266b..888ec552 100644 --- a/veadk/tracing/telemetry/opentelemetry_tracer.py +++ b/veadk/tracing/telemetry/opentelemetry_tracer.py @@ -18,7 +18,7 @@ import time from typing import Any -from openinference.instrumentation.google_adk import GoogleADKInstrumentor +# from openinference.instrumentation.google_adk import GoogleADKInstrumentor from opentelemetry import trace as trace_api from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.sdk import trace as trace_sdk @@ -79,7 +79,7 @@ def model_post_init(self, context: Any) -> None: # VeADK operates on global OpenTelemetry provider, return nothing self._init_global_tracer_provider() - GoogleADKInstrumentor().instrument() + # GoogleADKInstrumentor().instrument() def _init_global_tracer_provider(self) -> None: # set provider anyway, then get global provider @@ -132,7 +132,7 @@ def trace_file_path(self) -> str: @property def trace_id(self) -> str: try: - trace_id = hex(int(self._inmemory_exporter._exporter.trace_id))[2:] + trace_id = hex(int(self._inmemory_exporter._exporter.trace_id))[2:] # type: ignore return trace_id except Exception as e: logger.error(f"Failed to get trace_id from InMemoryExporter: {e}") @@ -151,23 +151,11 @@ def dump( ) return "" - prompt_tokens = self._inmemory_exporter._real_exporter.prompt_tokens - completion_tokens = self._inmemory_exporter._real_exporter.completion_tokens - - # upload - for meter_uploader in self._meter_uploaders: - meter_uploader.record( - prompt_tokens=prompt_tokens, completion_tokens=completion_tokens - ) - # clear tokens after dump - self._inmemory_exporter._real_exporter.completion_tokens = [] - self._inmemory_exporter._real_exporter.prompt_tokens = [] - for processor in self._processors: time.sleep(0.05) # give some time for the exporter to upload spans processor.force_flush() - spans = self._inmemory_exporter._real_exporter.get_finished_spans( + spans = self._inmemory_exporter._exporter.get_finished_spans( # type: ignore session_id=session_id ) if not spans: @@ -186,14 +174,11 @@ def dump( for s in spans ] - trace_id = hex(int(self._inmemory_exporter._real_exporter.trace_id))[2:] - self._trace_id = trace_id - file_path = f"{path}/{self.name}_{user_id}_{session_id}_{trace_id}.json" + file_path = f"{path}/{self.name}_{user_id}_{session_id}_{self.trace_id}.json" with open(file_path, "w") as f: json.dump( data, f, indent=4, ensure_ascii=False ) # ensure_ascii=False to support Chinese characters - self._trace_file_path = file_path for exporter in self.exporters: @@ -203,6 +188,6 @@ def dump( f"OpenTelemetryTracer tracing done, trace id: {self._trace_id} (hex)" ) - self._spans = spans logger.info(f"OpenTelemetryTracer dumps {len(spans)} spans to {file_path}") + return file_path diff --git a/veadk/tracing/telemetry/telemetry.py b/veadk/tracing/telemetry/telemetry.py index 494f215e..7e2442c5 100644 --- a/veadk/tracing/telemetry/telemetry.py +++ b/veadk/tracing/telemetry/telemetry.py @@ -5,27 +5,33 @@ from google.adk.models.llm_request import LlmRequest from google.adk.models.llm_response import LlmResponse from google.adk.tools import BaseTool -from google.genai import types from opentelemetry import trace +from opentelemetry.trace.span import Span from veadk.tracing.telemetry.attributes.attributes import ATTRIBUTES -from veadk.tracing.telemetry.attributes.extractors.llm_attributes_extrators import ( +from veadk.tracing.telemetry.attributes.extractors.llm_attributes_extractors import ( LLMAttributesParams, ) +from veadk.tracing.telemetry.attributes.extractors.tool_attributes_extractors import ( + ToolAttributesParams, +) from veadk.utils.logger import get_logger logger = get_logger(__name__) -def _set_common_attributes(): - pass +def _set_common_attributes( + span: Span, app_name: str, user_id: str, session_id: str +) -> None: + common_attributes = ATTRIBUTES.get("common", {}) + for attr_name, attr_extractor in common_attributes.items(): + value = attr_extractor( + app_name=app_name, user_id=user_id, session_id=session_id + ) + span.set_attribute(attr_name, value) -def trace_send_data( - invocation_context: InvocationContext, - event_id: str, - data: list[types.Content], -) -> None: ... +def trace_send_data(): ... def trace_tool_call( @@ -33,7 +39,20 @@ def trace_tool_call( args: dict[str, Any], function_response_event: Event, ) -> None: - print("tool_call") + span = trace.get_current_span() + + tool_attributes = ATTRIBUTES.get("tool", {}) + for attr_name, attr_extractor in tool_attributes.items(): + params = ToolAttributesParams(tool, args, function_response_event) + # set attribute anyway + value = attr_extractor(params) + if isinstance(value, list): + for _value in value: + for key, val in _value.items(): + # gen_ai. and gen_ai_ + span.set_attribute(f"{attr_name}{key}", val) + else: + span.set_attribute(attr_name, value) def trace_call_llm( @@ -44,8 +63,14 @@ def trace_call_llm( ) -> None: span = trace.get_current_span() - llm_attributes = ATTRIBUTES.get("llm", {}) + _set_common_attributes( + span=span, + app_name=invocation_context.session.app_name, + user_id=invocation_context.session.user_id, + session_id=invocation_context.session.id, + ) + llm_attributes = ATTRIBUTES.get("llm", {}) for attr_name, attr_extractor in llm_attributes.items(): params = LLMAttributesParams( invocation_context, event_id, llm_request, llm_response @@ -56,7 +81,6 @@ def trace_call_llm( for _value in value: for key, val in _value.items(): # gen_ai. and gen_ai_ - logger.debug(f"Set attribute {attr_name}{key} = {val}") span.set_attribute(f"{attr_name}{key}", val) else: span.set_attribute(attr_name, value) From 773cfbae5acfbbf7b30fee6f38be347d1518aa5b Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Fri, 22 Aug 2025 11:00:08 +0800 Subject: [PATCH 04/20] realize invocation and agent run attributes --- .../common_attributes_extractors.py | 6 ++ .../telemetry/exporters/inmemory_exporter.py | 37 +++++++++++- .../tracing/telemetry/opentelemetry_tracer.py | 58 ++++++++++++++----- veadk/tracing/telemetry/telemetry.py | 40 ++++++++----- 4 files changed, 108 insertions(+), 33 deletions(-) diff --git a/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py index 5d5d6aab..e627f130 100644 --- a/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py @@ -14,6 +14,11 @@ def common_gen_ai_app_name(**kwargs) -> str: return app_name or "" +def common_gen_ai_agent_name(**kwargs) -> str: + agent_name = kwargs.get("agent_name") + return agent_name or "" + + def common_gen_ai_user_id(**kwargs) -> str: user_id = kwargs.get("user_id") return user_id or "" @@ -28,6 +33,7 @@ def common_gen_ai_session_id(**kwargs) -> str: "gen_ai.system": common_gen_ai_system, "gen_ai.system.version": common_gen_ai_system_version, "gen_ai.app.name": common_gen_ai_app_name, + "gen_ai.agent.name": common_gen_ai_agent_name, "gen_ai.user.id": common_gen_ai_user_id, "gen_ai.session.id": common_gen_ai_session_id, } diff --git a/veadk/tracing/telemetry/exporters/inmemory_exporter.py b/veadk/tracing/telemetry/exporters/inmemory_exporter.py index af8fff42..b54d0ca5 100644 --- a/veadk/tracing/telemetry/exporters/inmemory_exporter.py +++ b/veadk/tracing/telemetry/exporters/inmemory_exporter.py @@ -14,6 +14,12 @@ from typing import Sequence +from opentelemetry.context import ( + _SUPPRESS_INSTRUMENTATION_KEY, + attach, + detach, + set_value, +) from opentelemetry.sdk.trace import ReadableSpan, export from typing_extensions import override @@ -23,7 +29,7 @@ logger = get_logger(__name__) -in_memory_exporter_instance = None +inmemory_span_processor = None # ======== Adapted from Google ADK ======== @@ -64,6 +70,30 @@ def clear(self): self._spans.clear() +class _InMemorySpanProcessor(export.SimpleSpanProcessor): + def __init__(self, exporter: _InMemoryExporter) -> None: + super().__init__(exporter) + self.spans = [] + + def on_start(self, span, parent_context) -> None: + if span.context: + self.spans.append(span) + + def on_end(self, span: ReadableSpan) -> None: + if span.context: + if not span.context.trace_flags.sampled: + return + token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) + try: + self.span_exporter.export((span,)) + # pylint: disable=broad-exception-caught + except Exception: + logger.exception("Exception while exporting Span.") + detach(token) + if span in self.spans: + self.spans.remove(span) + + class InMemoryExporter(BaseExporter): """InMemory Exporter mainly for store spans in memory for debugging / observability purposes.""" @@ -73,4 +103,7 @@ def __init__(self, name: str = "inmemory_exporter") -> None: self.name = name self._exporter = _InMemoryExporter() - self.processor = export.SimpleSpanProcessor(self._exporter) + self.processor = _InMemorySpanProcessor(self._exporter) + + global inmemory_span_processor + inmemory_span_processor = self.processor diff --git a/veadk/tracing/telemetry/opentelemetry_tracer.py b/veadk/tracing/telemetry/opentelemetry_tracer.py index 888ec552..251c3bb8 100644 --- a/veadk/tracing/telemetry/opentelemetry_tracer.py +++ b/veadk/tracing/telemetry/opentelemetry_tracer.py @@ -25,7 +25,7 @@ from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor -from pydantic import BaseModel, ConfigDict, Field, field_validator +from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator from typing_extensions import override from veadk.tracing.base_tracer import BaseTracer @@ -37,8 +37,6 @@ logger = get_logger(__name__) -DEFAULT_VEADK_TRACER_NAME = "veadk_global_tracer" - def update_resource_attributions(provider: TracerProvider, resource_attributes: dict): provider._resource = provider._resource.merge(Resource.create(resource_attributes)) @@ -47,20 +45,37 @@ def update_resource_attributions(provider: TracerProvider, resource_attributes: class OpentelemetryTracer(BaseModel, BaseTracer): model_config = ConfigDict(arbitrary_types_allowed=True) - name: str = Field( - default=DEFAULT_VEADK_TRACER_NAME, description="The identifier of tracer." - ) - - app_name: str = Field( - default="veadk_app", - description="The identifier of app.", - ) + name: str = Field(default="veadk_tracer", description="The identifier of tracer.") exporters: list[BaseExporter] = Field( default_factory=list, description="The exporters to export spans.", ) + _app_name: str = PrivateAttr(default="") + + _agent_name: str = PrivateAttr(default="") + + @property + def app_name(self) -> str: + return self._app_name + + @app_name.setter + def app_name(self, value: str) -> None: + self._app_name = value + # update_common_attributes(self._tracer_provider, {"app_name": self._app_name}) + + @property + def agent_name(self) -> str: + return self._agent_name + + @agent_name.setter + def agent_name(self, value: str) -> None: + self._agent_name = value + # update_common_attributes( + # self._tracer_provider, {"agent_name": self._agent_name} + # ) + @field_validator("exporters") @classmethod def forbid_inmemory_exporter(cls, v: list[BaseExporter]) -> list[BaseExporter]: @@ -73,8 +88,6 @@ def model_post_init(self, context: Any) -> None: patch_google_adk_telemetry() self._processors = [] - self._inmemory_exporter = InMemoryExporter() - self.exporters.append(self._inmemory_exporter) # VeADK operates on global OpenTelemetry provider, return nothing self._init_global_tracer_provider() @@ -121,9 +134,22 @@ def _init_global_tracer_provider(self) -> None: f"Add span processor for exporter `{exporter.__class__.__name__}` to OpentelemetryTracer failed." ) - logger.debug( - f"Init OpentelemetryTracer with {len(self._processors)} exporters." - ) + self._inmemory_exporter = InMemoryExporter() + self._inmemory_exporter_processor = self._inmemory_exporter.processor + + # make sure the in memory exporter processor is added at index 0 + if self._inmemory_exporter_processor: + global_tracer_provider._active_span_processor._span_processors = ( + self._inmemory_exporter_processor, + ) + global_tracer_provider._active_span_processor._span_processors + + self._processors.append(self._inmemory_exporter_processor) + else: + logger.warning( + "InMemoryExporter processor is not initialized, cannot add to OpentelemetryTracer." + ) + + logger.info(f"Init OpentelemetryTracer with {len(self._processors)} exporters.") @property def trace_file_path(self) -> str: diff --git a/veadk/tracing/telemetry/telemetry.py b/veadk/tracing/telemetry/telemetry.py index 7e2442c5..a3436002 100644 --- a/veadk/tracing/telemetry/telemetry.py +++ b/veadk/tracing/telemetry/telemetry.py @@ -6,7 +6,7 @@ from google.adk.models.llm_response import LlmResponse from google.adk.tools import BaseTool from opentelemetry import trace -from opentelemetry.trace.span import Span +from opentelemetry.sdk.trace import _Span from veadk.tracing.telemetry.attributes.attributes import ATTRIBUTES from veadk.tracing.telemetry.attributes.extractors.llm_attributes_extractors import ( @@ -15,23 +15,32 @@ from veadk.tracing.telemetry.attributes.extractors.tool_attributes_extractors import ( ToolAttributesParams, ) +from veadk.tracing.telemetry.exporters import inmemory_exporter from veadk.utils.logger import get_logger logger = get_logger(__name__) -def _set_common_attributes( - span: Span, app_name: str, user_id: str, session_id: str -) -> None: - common_attributes = ATTRIBUTES.get("common", {}) - for attr_name, attr_extractor in common_attributes.items(): - value = attr_extractor( - app_name=app_name, user_id=user_id, session_id=session_id - ) - span.set_attribute(attr_name, value) +def trace_send_data(): ... -def trace_send_data(): ... +def set_common_attributes(current_span: _Span, **kwargs) -> None: + if current_span.context: + current_span_id = current_span.context.trace_id + + spans = inmemory_exporter.inmemory_span_processor.spans + + spans_in_current_trace = [ + span + for span in spans + if span.context and span.context.trace_id == current_span_id + ] + + common_attributes = ATTRIBUTES.get("common", {}) + for span in spans_in_current_trace: + for attr_name, attr_extractor in common_attributes.items(): + value = attr_extractor(**kwargs) + span.set_attribute(attr_name, value) def trace_tool_call( @@ -63,10 +72,11 @@ def trace_call_llm( ) -> None: span = trace.get_current_span() - _set_common_attributes( - span=span, - app_name=invocation_context.session.app_name, - user_id=invocation_context.session.user_id, + set_common_attributes( + current_span=span, # type: ignore + agent_name=invocation_context.agent.name, + app_name=invocation_context.app_name, + user_id=invocation_context.user_id, session_id=invocation_context.session.id, ) From 3026126d8f1252e5b8eec8611d37209afe6fa34c Mon Sep 17 00:00:00 2001 From: "hanzhi.421" Date: Fri, 22 Aug 2025 11:20:15 +0800 Subject: [PATCH 05/20] fix: fix the bugs in tracing so that it can pass the tests. --- tests/test_agent.py | 4 ---- tests/test_tracing.py | 6 +++--- veadk/tracing/telemetry/exporters/tls_exporter.py | 3 ++- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/test_agent.py b/tests/test_agent.py index 622e5caa..2843d03e 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -48,7 +48,3 @@ def test_agent(): assert agent.long_term_memory.backend == "local" assert load_memory in agent.tools - - assert tracer.tracer_hook_before_model in agent.before_model_callback - assert tracer.tracer_hook_after_model in agent.after_model_callback - assert tracer.tracer_hook_after_tool in agent.after_tool_callback diff --git a/tests/test_tracing.py b/tests/test_tracing.py index 635cfd45..333945d4 100644 --- a/tests/test_tracing.py +++ b/tests/test_tracing.py @@ -82,7 +82,7 @@ async def test_tracing(): exporters = init_exporters() tracer = OpentelemetryTracer(exporters=exporters) - assert len(tracer.exporters) == 4 # with extra 1 built-in exporters + assert len(tracer.exporters) == 3 # TODO: Ensure the tracing provider is set correctly after loading SDK @@ -98,7 +98,7 @@ async def test_tracing_with_global_provider(): # tracer = OpentelemetryTracer(exporters=exporters) - assert len(tracer.exporters) == 4 # with extra 1 built-in exporters + assert len(tracer.exporters) == 3 # with extra 1 built-in exporters @pytest.mark.asyncio @@ -113,4 +113,4 @@ async def test_tracing_with_apmplus_global_provider(): tracer = OpentelemetryTracer(exporters=exporters) # apmplus exporter won't init again - assert len(tracer.exporters) == 3 # with extra 1 built-in exporters + assert len(tracer.exporters) == 2 # with extra 1 built-in exporters diff --git a/veadk/tracing/telemetry/exporters/tls_exporter.py b/veadk/tracing/telemetry/exporters/tls_exporter.py index ba85b700..dddc6fb1 100644 --- a/veadk/tracing/telemetry/exporters/tls_exporter.py +++ b/veadk/tracing/telemetry/exporters/tls_exporter.py @@ -15,6 +15,7 @@ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.trace.export import BatchSpanProcessor from pydantic import BaseModel, Field +from typing import Any from typing_extensions import override from veadk.config import getenv @@ -47,7 +48,7 @@ class TLSExporterConfig(BaseModel): class TLSExporter(BaseExporter): config: TLSExporterConfig = Field(default_factory=TLSExporterConfig) - def model_post_init(self) -> None: + def model_post_init(self, context: Any) -> None: headers = { "x-tls-otel-tracetopic": self.config.topic_id, "x-tls-otel-ak": self.config.access_key, From e4b20b72256e86421ea9ca446d66db761cd01c1a Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Mon, 25 Aug 2025 09:27:51 +0800 Subject: [PATCH 06/20] update tracing --- veadk/agent.py | 7 - veadk/runner.py | 24 +- veadk/tools/builtin_tools/lark.py | 2 +- .../common_attributes_extractors.py | 2 +- .../extractors/llm_attributes_extractors.py | 390 +++++++++++++++--- .../extractors/tool_attributes_extractors.py | 20 +- .../telemetry/attributes/extractors/types.py | 59 +++ .../telemetry/exporters/inmemory_exporter.py | 13 +- .../tracing/telemetry/opentelemetry_tracer.py | 100 ++--- veadk/tracing/telemetry/telemetry.py | 48 +-- 10 files changed, 483 insertions(+), 182 deletions(-) create mode 100644 veadk/tracing/telemetry/attributes/extractors/types.py diff --git a/veadk/agent.py b/veadk/agent.py index f73eaca4..0da0d749 100644 --- a/veadk/agent.py +++ b/veadk/agent.py @@ -112,10 +112,6 @@ def model_post_init(self, __context: Any) -> None: self.tools.append(load_memory) - # if self.tracers: - # for tracer in self.tracers: - # tracer.do_hooks(self) - logger.info(f"{self.__class__.__name__} `{self.name}` init done.") logger.debug( f"Agent: {self.model_dump(include={'name', 'model_name', 'model_api_base', 'tools', 'serve_url'})}" @@ -215,9 +211,6 @@ async def run( session_service=session_service, memory_service=self.long_term_memory, ) - if getattr(self, "tracers", None): - for tracer in self.tracers: - tracer.set_app_name(app_name) logger.info(f"Begin to process prompt {prompt}") # run diff --git a/veadk/runner.py b/veadk/runner.py index 443b2e4f..06f49038 100644 --- a/veadk/runner.py +++ b/veadk/runner.py @@ -67,8 +67,6 @@ def __init__( # prevent VeRemoteAgent has no long-term memory attr if isinstance(self.agent, Agent): self.long_term_memory = self.agent.long_term_memory - # for tracer in self.agent.tracers: - # tracer.set_app_name(self.app_name) else: self.long_term_memory = None @@ -175,7 +173,27 @@ async def run( return final_output - def _print_trace_id(self): + def get_trace_id(self) -> str: + if not isinstance(self.agent, Agent): + logger.warning( + ("The agent is not an instance of VeADK Agent, no trace id provided.") + ) + return "" + + if not self.agent.tracers: + logger.warning( + "No tracer is configured in the agent, no trace id provided." + ) + return "" + + try: + trace_id = self.agent.tracers[0].trace_id # type: ignore + return trace_id + except Exception as e: + logger.warning(f"Get tracer id failed as {e}") + return "" + + def _print_trace_id(self) -> None: if not isinstance(self.agent, Agent): logger.warning( ("The agent is not an instance of VeADK Agent, no trace id provided.") diff --git a/veadk/tools/builtin_tools/lark.py b/veadk/tools/builtin_tools/lark.py index 406afb6f..a94acc23 100644 --- a/veadk/tools/builtin_tools/lark.py +++ b/veadk/tools/builtin_tools/lark.py @@ -46,7 +46,7 @@ def check_env(): ) from e -check_env() +# check_env() lark_tools = MCPToolset( connection_params=StdioServerParameters( diff --git a/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py index e627f130..40d4646b 100644 --- a/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py @@ -32,8 +32,8 @@ def common_gen_ai_session_id(**kwargs) -> str: COMMON_ATTRIBUTES = { "gen_ai.system": common_gen_ai_system, "gen_ai.system.version": common_gen_ai_system_version, - "gen_ai.app.name": common_gen_ai_app_name, "gen_ai.agent.name": common_gen_ai_agent_name, + "gen_ai.app.name": common_gen_ai_app_name, "gen_ai.user.id": common_gen_ai_user_id, "gen_ai.session.id": common_gen_ai_session_id, } diff --git a/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py index 79d7e9dc..b7ec1950 100644 --- a/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py @@ -1,108 +1,365 @@ -from attr import dataclass -from google.adk.agents.invocation_context import InvocationContext -from google.adk.models.llm_request import LlmRequest -from google.adk.models.llm_response import LlmResponse +import json -@dataclass -class LLMAttributesParams: - invocation_context: InvocationContext - event_id: str - llm_request: LlmRequest - llm_response: LlmResponse +from veadk.tracing.telemetry.attributes.extractors.types import ( + ExtractorResponse, + LLMAttributesParams, +) -def llm_gen_ai_request_model(params: LLMAttributesParams) -> str: - return params.llm_request.model or "" +def flatten(d, parent_key="", sep="."): + items = [] + if isinstance(d, dict): + for k, v in d.items(): + new_key = f"{parent_key}{sep}{k}" if parent_key else k + items.extend(flatten(v, new_key, sep=sep).items()) + elif isinstance(d, list): + if not d: # empty list + items.append((parent_key, "")) + else: + for i, v in enumerate(d): + new_key = f"{parent_key}{sep}{i}" if parent_key else str(i) + items.extend(flatten(v, new_key, sep=sep).items()) + else: + items.append((parent_key, d)) + return dict(items) -def llm_gen_ai_request_type(params: LLMAttributesParams) -> str | list[str]: - type = "completion" - return type or "" +def llm_gen_ai_request_model(params: LLMAttributesParams) -> ExtractorResponse: + return ExtractorResponse(content=params.llm_request.model or "") -def llm_gen_ai_response_model(params: LLMAttributesParams) -> str: - return params.llm_request.model or "" +def llm_gen_ai_request_type(params: LLMAttributesParams) -> ExtractorResponse: + return ExtractorResponse(content=type or "") -def llm_gen_ai_request_max_tokens(params: LLMAttributesParams) -> int | None: - return params.llm_request.config.max_output_tokens +def llm_gen_ai_response_model(params: LLMAttributesParams) -> ExtractorResponse: + return ExtractorResponse(content=params.llm_request.model or "") -def llm_gen_ai_request_temperature(params: LLMAttributesParams) -> float | None: - return params.llm_request.config.temperature +def llm_gen_ai_request_max_tokens(params: LLMAttributesParams) -> ExtractorResponse: + return ExtractorResponse(content=params.llm_request.config.max_output_tokens) -def llm_gen_ai_request_top_p(params: LLMAttributesParams) -> float | None: - return params.llm_request.config.top_p +def llm_gen_ai_request_temperature(params: LLMAttributesParams) -> ExtractorResponse: + return ExtractorResponse(content=params.llm_request.config.temperature) -def llm_gen_ai_prompt(params: LLMAttributesParams) -> list[dict]: - ret = [] - for idx, content in enumerate(params.llm_request.contents): - if content.parts: - role = content.role - parts = [part for part in content.parts if not part.inline_data] - ret.append({f".{idx}.role": role, f".{idx}.content": str(parts)}) - return ret - +def llm_gen_ai_request_top_p(params: LLMAttributesParams) -> ExtractorResponse: + return ExtractorResponse(content=params.llm_request.config.top_p) -def llm_gen_ai_completion(params: LLMAttributesParams) -> list[dict] | None: - ret = [] - content = params.llm_response.content - if content and content.parts: - parts = [part for part in content.parts if not part.inline_data] - ret.append({f".{0}.role": content.role, f".{0}.content": str(parts)}) - return ret +def llm_gen_ai_response_stop_reason(params: LLMAttributesParams) -> ExtractorResponse: + return ExtractorResponse(content="") -def llm_gen_ai_response_stop_reason(params: LLMAttributesParams) -> str | None: - return "" - - -def llm_gen_ai_response_finish_reason(params: LLMAttributesParams) -> str | None: +def llm_gen_ai_response_finish_reason(params: LLMAttributesParams) -> ExtractorResponse: # TODO: update to google-adk v1.12.0 - return None + return ExtractorResponse(content="") -def llm_gen_ai_usage_input_tokens(params: LLMAttributesParams) -> int | None: +def llm_gen_ai_usage_input_tokens(params: LLMAttributesParams) -> ExtractorResponse: if params.llm_response.usage_metadata: - return params.llm_response.usage_metadata.prompt_token_count - return None + return ExtractorResponse( + content=params.llm_response.usage_metadata.prompt_token_count + ) + return ExtractorResponse(content=None) -def llm_gen_ai_usage_output_tokens(params: LLMAttributesParams) -> int | None: +def llm_gen_ai_usage_output_tokens(params: LLMAttributesParams) -> ExtractorResponse: if params.llm_response.usage_metadata: - return params.llm_response.usage_metadata.candidates_token_count - return None + return ExtractorResponse( + content=params.llm_response.usage_metadata.candidates_token_count, + ) + return ExtractorResponse(content=None) -def llm_gen_ai_usage_total_tokens(params: LLMAttributesParams) -> int | None: +def llm_gen_ai_usage_total_tokens(params: LLMAttributesParams) -> ExtractorResponse: if params.llm_response.usage_metadata: - return params.llm_response.usage_metadata.total_token_count - return None + return ExtractorResponse( + content=params.llm_response.usage_metadata.total_token_count, + ) + return ExtractorResponse(content=None) +# FIXME def llm_gen_ai_usage_cache_creation_input_tokens( params: LLMAttributesParams, -) -> int | None: +) -> ExtractorResponse: if params.llm_response.usage_metadata: - return None - # return params.llm_response.usage_metadata.cached_content_token_count - return None + return ExtractorResponse( + content=params.llm_response.usage_metadata.cached_content_token_count, + ) + return ExtractorResponse(content=None) -def llm_gen_ai_usage_cache_read_input_tokens(params: LLMAttributesParams) -> int | None: +# FIXME +def llm_gen_ai_usage_cache_read_input_tokens( + params: LLMAttributesParams, +) -> ExtractorResponse: if params.llm_response.usage_metadata: - return None - # return params.llm_response.usage_metadata.prompt_token_count - return None + return ExtractorResponse( + content=params.llm_response.usage_metadata.cached_content_token_count, + ) + return ExtractorResponse(content=None) -def llm_gen_ai_is_streaming(params: LLMAttributesParams) -> bool | None: +def llm_gen_ai_prompt(params: LLMAttributesParams) -> ExtractorResponse: + # a content is a message + messages: list[dict] = [] + + for content in params.llm_request.contents: + if content.parts: + for idx, part in enumerate(content.parts): + message = {} + # text part + if part.text: + message[f"gen_ai.prompt.{idx}.role"] = content.role + message[f"gen_ai.prompt.{idx}.content"] = part.text + # function response + if part.function_response: + message[f"gen_ai.prompt.{idx}.role"] = content.role + message[f"gen_ai.prompt.{idx}.content"] = str( + content.parts[0].function_response + ) + # function call + if part.function_call: + message[f"gen_ai.prompt.{idx}.tool_calls.0.id"] = ( + part.function_call.id + if part.function_call.id + else "" + ) + message[f"gen_ai.prompt.{idx}.tool_calls.0.type"] = "function" + message[f"gen_ai.prompt.{idx}.tool_calls.0.function.name"] = ( + part.function_call.name + if part.function_call.name + else "" + ) + message[f"gen_ai.prompt.{idx}.tool_calls.0.function.arguments"] = ( + json.dumps(part.function_call.args) + if part.function_call.args + else json.dumps({}) + ) + + if message: + messages.append(message) + + return ExtractorResponse(content=messages) + + +def llm_gen_ai_completion(params: LLMAttributesParams) -> ExtractorResponse: + messages = [] + + content = params.llm_response.content + if content and content.parts: + for idx, part in enumerate(content.parts): + message = {} + if part.text: + message[f"gen_ai.completion.{idx}.role"] = content.role + message[f"gen_ai.completion.{idx}.content"] = part.text + elif part.function_call: + message[f"gen_ai.completion.{idx}.role"] = content.role + message[f"gen_ai.completion.{idx}.tool_calls.0.id"] = ( + part.function_call.id + if part.function_call.id + else "" + ) + message[f"gen_ai.completion.{idx}.tool_calls.0.type"] = "function" + message[f"gen_ai.completion.{idx}.tool_calls.0.function.name"] = ( + part.function_call.name + if part.function_call.name + else "" + ) + message[f"gen_ai.completion.{idx}.tool_calls.0.function.arguments"] = ( + json.dumps(part.function_call.args) + if part.function_call.args + else json.dumps({}) + ) + + if message: + messages.append(message) + return ExtractorResponse(content=messages) + + +def llm_gen_ai_is_streaming(params: LLMAttributesParams) -> ExtractorResponse: # return params.llm_request.stream - return None + return ExtractorResponse(content=None) + + +def llm_gen_ai_operation_name(params: LLMAttributesParams) -> ExtractorResponse: + return ExtractorResponse(content="chat") + + +def llm_gen_ai_system_message(params: LLMAttributesParams) -> ExtractorResponse: + event_attributes = { + "content": str(params.llm_request.config.system_instruction), + "role": "system", + } + return ExtractorResponse(type="event", content=event_attributes) + + +def llm_gen_ai_user_message(params: LLMAttributesParams) -> ExtractorResponse: + # a content is a message + messages = [] + + for content in params.llm_request.contents: + if content.role == "user": + message_parts = [] + + if content.parts: + if len(content.parts) == 1: + if content.parts[0].text: + message_parts.append( + { + "role": content.role, + "content": content.parts[0].text, + } + ) + elif content.parts[0].function_response: + message_parts.append( + { + "role": content.role, + "content": str( + content.parts[0].function_response.response + ), + } + ) + else: + message_part = {"role": content.role} + for idx, part in enumerate(content.parts): + # text part + if part.text: + message_part[f"parts.{idx}.type"] = "text" + message_part[f"parts.{idx}.content"] = part.text + # function response + if part.function_response: + message_part[f"parts.{idx}.type"] = "function" + message_part[f"parts.{idx}.content"] = str( + content.parts[0].function_response + ) + + message_parts.append(message_part) + + if message_parts: + messages.extend(message_parts) + + return ExtractorResponse(type="event", content=messages) + + +def llm_gen_ai_assistant_message(params: LLMAttributesParams) -> ExtractorResponse: + # a content is a message + messages = [] + + # each part in each content we make it a message + # e.g. 2 contents and 3 parts each means 6 messages + for content in params.llm_request.contents: + if content.role == "model": + message_parts = [] + + # each part we make it a message + if content.parts: + # only one part + if len(content.parts) == 1: + if content.parts[0].text: + message_parts.append( + { + "role": content.role, + "content": content.parts[0].text, + } + ) + elif content.parts[0].function_call: + pass + # multiple parts + else: + message_part = {"role": content.role} + + for idx, part in enumerate(content.parts): + # parse content + if part.text: + message_part[f"parts.{idx}.type"] = "text" + message_part[f"parts.{idx}.content"] = part.text + # parse tool_calls + if part.function_call: + message_part["tool_calls.0.id"] = ( + part.function_call.id + if part.function_call.id + else "" + ) + message_part["tool_calls.0.type"] = "function" + message_part["tool_calls.0.function.name"] = ( + part.function_call.name + if part.function_call.name + else "" + ) + message_part["tool_calls.0.function.arguments"] = ( + json.dumps(part.function_call.args) + if part.function_call.args + else json.dumps({}) + ) + message_parts.append(message_part) + + if message_parts: + messages.extend(message_parts) + + return ExtractorResponse(type="event", content=messages) + + +def llm_gen_ai_choice(params: LLMAttributesParams) -> ExtractorResponse: + message = {} + + # parse content to build a message + content = params.llm_response.content + if content and content.parts: + message = {"message.role": content.role} + + if len(content.parts) == 1: + part = content.parts[0] + if part.text: + message["message.content"] = part.text + elif part.function_call: + message["message.tool_calls.0.id"] = ( + part.function_call.id + if part.function_call.id + else "" + ) + message["message.tool_calls.0.type"] = "function" + message["message.tool_calls.0.function.name"] = ( + part.function_call.name + if part.function_call.name + else "" + ) + message["message.tool_calls.0.function.arguments"] = ( + json.dumps(part.function_call.args) + if part.function_call.args + else json.dumps({}) + ) + else: + for idx, part in enumerate(content.parts): + # parse content + if part.text: + message[f"message.parts.{idx}.type"] = "text" + message[f"message.parts.{idx}.text"] = part.text + + # parse tool_calls + if part.function_call: + message["message.tool_calls.0.id"] = ( + part.function_call.id + if part.function_call.id + else "" + ) + message["message.tool_calls.0.type"] = "function" + message["message.tool_calls.0.function.name"] = ( + part.function_call.name + if part.function_call.name + else "" + ) + message["message.tool_calls.0.function.arguments"] = ( + json.dumps(part.function_call.args) + if part.function_call.args + else json.dumps({}) + ) + + return ExtractorResponse(type="event", content=message) LLM_ATTRIBUTES = { @@ -122,4 +379,9 @@ def llm_gen_ai_is_streaming(params: LLMAttributesParams) -> bool | None: "gen_ai.usage.cache_creation_input_tokens": llm_gen_ai_usage_cache_creation_input_tokens, "gen_ai.usage.cache_read_input_tokens": llm_gen_ai_usage_cache_read_input_tokens, "gen_ai.is_streaming": llm_gen_ai_is_streaming, + "gen_ai.operation.name": llm_gen_ai_operation_name, + "gen_ai.system.message": llm_gen_ai_system_message, + "gen_ai.user.message": llm_gen_ai_user_message, + "gen_ai.assistant.message": llm_gen_ai_assistant_message, + "gen_ai.choice": llm_gen_ai_choice, } diff --git a/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py index b68a9c49..db6b9709 100644 --- a/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py @@ -1,15 +1,13 @@ -from typing import Any +from veadk.tracing.telemetry.attributes.extractors.types import ( + ExtractorResponse, + ToolAttributesParams, +) -from attr import dataclass -from google.adk.events import Event -from google.adk.tools import BaseTool +def tool_gen_ai_operation_name(params: ToolAttributesParams) -> ExtractorResponse: + return ExtractorResponse(content="execute_tool") -@dataclass -class ToolAttributesParams: - tool: BaseTool - args: dict[str, Any] - function_response_event: Event - -TOOL_ATTRIBUTES = {} +TOOL_ATTRIBUTES = { + "gen_ai.operation.name": tool_gen_ai_operation_name, +} diff --git a/veadk/tracing/telemetry/attributes/extractors/types.py b/veadk/tracing/telemetry/attributes/extractors/types.py new file mode 100644 index 00000000..ea8931b6 --- /dev/null +++ b/veadk/tracing/telemetry/attributes/extractors/types.py @@ -0,0 +1,59 @@ +from typing import Any, Literal + +from attr import dataclass +from google.adk.agents.invocation_context import InvocationContext +from google.adk.events import Event +from google.adk.models.llm_request import LlmRequest +from google.adk.models.llm_response import LlmResponse +from google.adk.tools import BaseTool +from opentelemetry.sdk.trace import _Span +from opentelemetry.trace.span import Span + + +@dataclass +class ExtractorResponse: + content: Any + + type: Literal["attribute", "event"] = "attribute" + """Type of extractor response. + + `raw`: span[...] = raw + `event`: span.add_event(...) = ... + """ + + @staticmethod + def update_span( + span: _Span | Span, attr_name: str, response: "ExtractorResponse" + ) -> None: + if response.type == "attribute": + res = response.content + if isinstance(res, list): # list[dict] + for _res in res: + if isinstance(_res, dict): + for k, v in _res.items(): + span.set_attribute(k, v) + else: + span.set_attribute(attr_name, res) + elif response.type == "event": + if isinstance(response.content, dict): + span.add_event(attr_name, response.content) + else: # list + for event in response.content: + span.add_event(attr_name, event) + else: + pass + + +@dataclass +class LLMAttributesParams: + invocation_context: InvocationContext + event_id: str + llm_request: LlmRequest + llm_response: LlmResponse + + +@dataclass +class ToolAttributesParams: + tool: BaseTool + args: dict[str, Any] + function_response_event: Event diff --git a/veadk/tracing/telemetry/exporters/inmemory_exporter.py b/veadk/tracing/telemetry/exporters/inmemory_exporter.py index b54d0ca5..5e210a34 100644 --- a/veadk/tracing/telemetry/exporters/inmemory_exporter.py +++ b/veadk/tracing/telemetry/exporters/inmemory_exporter.py @@ -38,7 +38,6 @@ def __init__(self) -> None: super().__init__() self._spans = [] self.trace_id = "" - self.span_dict = {} self.session_trace_dict = {} @override @@ -46,13 +45,19 @@ def export(self, spans: Sequence[ReadableSpan]) -> export.SpanExportResult: for span in spans: if span.context: self.trace_id = span.context.trace_id - - span_id = span.context.span_id - self.span_dict[span_id] = span else: logger.warning( f"Span context is missing, failed to get `trace_id`. span: {span}" ) + + if span.name == "call_llm": + attributes = dict(span.attributes or {}) + session_id = attributes.get("gen_ai.session.id", None) + if session_id: + if session_id not in self.session_trace_dict: + self.session_trace_dict[session_id] = [self.trace_id] + else: + self.session_trace_dict[session_id] += [self.trace_id] self._spans.extend(spans) return export.SpanExportResult.SUCCESS diff --git a/veadk/tracing/telemetry/opentelemetry_tracer.py b/veadk/tracing/telemetry/opentelemetry_tracer.py index 251c3bb8..9f1019b9 100644 --- a/veadk/tracing/telemetry/opentelemetry_tracer.py +++ b/veadk/tracing/telemetry/opentelemetry_tracer.py @@ -18,14 +18,13 @@ import time from typing import Any -# from openinference.instrumentation.google_adk import GoogleADKInstrumentor from opentelemetry import trace as trace_api from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.sdk import trace as trace_sdk from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor -from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator +from pydantic import BaseModel, ConfigDict, Field, field_validator from typing_extensions import override from veadk.tracing.base_tracer import BaseTracer @@ -38,44 +37,24 @@ logger = get_logger(__name__) -def update_resource_attributions(provider: TracerProvider, resource_attributes: dict): +def _update_resource_attributions( + provider: TracerProvider, resource_attributes: dict +) -> None: provider._resource = provider._resource.merge(Resource.create(resource_attributes)) class OpentelemetryTracer(BaseModel, BaseTracer): model_config = ConfigDict(arbitrary_types_allowed=True) - name: str = Field(default="veadk_tracer", description="The identifier of tracer.") + name: str = Field( + default="veadk_opentelemetry_tracer", description="The identifier of tracer." + ) exporters: list[BaseExporter] = Field( default_factory=list, description="The exporters to export spans.", ) - _app_name: str = PrivateAttr(default="") - - _agent_name: str = PrivateAttr(default="") - - @property - def app_name(self) -> str: - return self._app_name - - @app_name.setter - def app_name(self, value: str) -> None: - self._app_name = value - # update_common_attributes(self._tracer_provider, {"app_name": self._app_name}) - - @property - def agent_name(self) -> str: - return self._agent_name - - @agent_name.setter - def agent_name(self, value: str) -> None: - self._agent_name = value - # update_common_attributes( - # self._tracer_provider, {"agent_name": self._agent_name} - # ) - @field_validator("exporters") @classmethod def forbid_inmemory_exporter(cls, v: list[BaseExporter]) -> list[BaseExporter]: @@ -86,18 +65,15 @@ def forbid_inmemory_exporter(cls, v: list[BaseExporter]) -> list[BaseExporter]: def model_post_init(self, context: Any) -> None: patch_google_adk_telemetry() - - self._processors = [] - - # VeADK operates on global OpenTelemetry provider, return nothing self._init_global_tracer_provider() - # GoogleADKInstrumentor().instrument() + # GoogleADKInstrumentor().instrument() def _init_global_tracer_provider(self) -> None: + self._processors = [] + # set provider anyway, then get global provider - tracer_provider = trace_sdk.TracerProvider() - trace_api.set_tracer_provider(tracer_provider) + trace_api.set_tracer_provider(trace_sdk.TracerProvider()) global_tracer_provider: TracerProvider = trace_api.get_tracer_provider() # type: ignore span_processors = global_tracer_provider._active_span_processor._span_processors @@ -118,7 +94,7 @@ def _init_global_tracer_provider(self) -> None: resource_attributes = exporter.resource_attributes if resource_attributes: - update_resource_attributions( + _update_resource_attributions( global_tracer_provider, resource_attributes ) @@ -135,15 +111,14 @@ def _init_global_tracer_provider(self) -> None: ) self._inmemory_exporter = InMemoryExporter() - self._inmemory_exporter_processor = self._inmemory_exporter.processor - - # make sure the in memory exporter processor is added at index 0 - if self._inmemory_exporter_processor: + if self._inmemory_exporter.processor: + # make sure the in memory exporter processor is added at index 0 + # because we use this to record all spans global_tracer_provider._active_span_processor._span_processors = ( - self._inmemory_exporter_processor, + self._inmemory_exporter.processor, ) + global_tracer_provider._active_span_processor._span_processors - self._processors.append(self._inmemory_exporter_processor) + self._processors.append(self._inmemory_exporter.processor) else: logger.warning( "InMemoryExporter processor is not initialized, cannot add to OpentelemetryTracer." @@ -164,30 +139,34 @@ def trace_id(self) -> str: logger.error(f"Failed to get trace_id from InMemoryExporter: {e}") return self._trace_id + def force_export(self) -> None: + """Force to export spans in all processors.""" + for processor in self._processors: + time.sleep(0.05) + processor.force_flush() + @override def dump( self, - user_id: str, - session_id: str, + user_id: str = "unknown_user_id", + session_id: str = "unknown_session_id", path: str = "/tmp", ) -> str: + def _build_trace_file_path(path: str, user_id: str, session_id: str) -> str: + return f"{path}/{self.name}_{user_id}_{session_id}_{self.trace_id}.json" + if not self._inmemory_exporter: logger.warning( "InMemoryExporter is not initialized. Please check your tracer exporters." ) return "" - - for processor in self._processors: - time.sleep(0.05) # give some time for the exporter to upload spans - processor.force_flush() + self.force_export() spans = self._inmemory_exporter._exporter.get_finished_spans( # type: ignore session_id=session_id ) - if not spans: - data = [] - else: - data = [ + data = ( + [ { "name": s.name, "span_id": s.context.span_id, @@ -199,21 +178,18 @@ def dump( } for s in spans ] + if spans + else [] + ) - file_path = f"{path}/{self.name}_{user_id}_{session_id}_{self.trace_id}.json" - with open(file_path, "w") as f: + self._trace_file_path = _build_trace_file_path(path, user_id, session_id) + with open(self._trace_file_path, "w") as f: json.dump( data, f, indent=4, ensure_ascii=False ) # ensure_ascii=False to support Chinese characters - self._trace_file_path = file_path - for exporter in self.exporters: - if not isinstance(exporter, InMemoryExporter): - exporter.export() logger.info( - f"OpenTelemetryTracer tracing done, trace id: {self._trace_id} (hex)" + f"OpenTelemetryTracer dumps {len(spans)} spans to {self._trace_file_path}. Trace id: {self.trace_id} (hex)" ) - logger.info(f"OpenTelemetryTracer dumps {len(spans)} spans to {file_path}") - - return file_path + return self._trace_file_path diff --git a/veadk/tracing/telemetry/telemetry.py b/veadk/tracing/telemetry/telemetry.py index a3436002..7cf2388f 100644 --- a/veadk/tracing/telemetry/telemetry.py +++ b/veadk/tracing/telemetry/telemetry.py @@ -9,10 +9,9 @@ from opentelemetry.sdk.trace import _Span from veadk.tracing.telemetry.attributes.attributes import ATTRIBUTES -from veadk.tracing.telemetry.attributes.extractors.llm_attributes_extractors import ( +from veadk.tracing.telemetry.attributes.extractors.types import ( + ExtractorResponse, LLMAttributesParams, -) -from veadk.tracing.telemetry.attributes.extractors.tool_attributes_extractors import ( ToolAttributesParams, ) from veadk.tracing.telemetry.exporters import inmemory_exporter @@ -50,18 +49,12 @@ def trace_tool_call( ) -> None: span = trace.get_current_span() - tool_attributes = ATTRIBUTES.get("tool", {}) - for attr_name, attr_extractor in tool_attributes.items(): - params = ToolAttributesParams(tool, args, function_response_event) - # set attribute anyway - value = attr_extractor(params) - if isinstance(value, list): - for _value in value: - for key, val in _value.items(): - # gen_ai. and gen_ai_ - span.set_attribute(f"{attr_name}{key}", val) - else: - span.set_attribute(attr_name, value) + tool_attributes_mapping = ATTRIBUTES.get("tool", {}) + params = ToolAttributesParams(tool, args, function_response_event) + + for attr_name, attr_extractor in tool_attributes_mapping.items(): + response: ExtractorResponse = attr_extractor(params) + ExtractorResponse.update_span(span, attr_name, response) def trace_call_llm( @@ -80,17 +73,14 @@ def trace_call_llm( session_id=invocation_context.session.id, ) - llm_attributes = ATTRIBUTES.get("llm", {}) - for attr_name, attr_extractor in llm_attributes.items(): - params = LLMAttributesParams( - invocation_context, event_id, llm_request, llm_response - ) - # set attribute anyway - value = attr_extractor(params) - if isinstance(value, list): - for _value in value: - for key, val in _value.items(): - # gen_ai. and gen_ai_ - span.set_attribute(f"{attr_name}{key}", val) - else: - span.set_attribute(attr_name, value) + llm_attributes_mapping = ATTRIBUTES.get("llm", {}) + params = LLMAttributesParams( + invocation_context=invocation_context, + event_id=event_id, + llm_request=llm_request, + llm_response=llm_response, + ) + + for attr_name, attr_extractor in llm_attributes_mapping.items(): + response: ExtractorResponse = attr_extractor(params) + ExtractorResponse.update_span(span, attr_name, response) From 3c30699ab8f2766a7188e97e042440e5a43758bb Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Mon, 25 Aug 2025 09:39:23 +0800 Subject: [PATCH 07/20] fix function response --- .../attributes/extractors/llm_attributes_extractors.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py index b7ec1950..0e96d719 100644 --- a/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py @@ -1,6 +1,5 @@ import json - from veadk.tracing.telemetry.attributes.extractors.types import ( ExtractorResponse, LLMAttributesParams, @@ -105,7 +104,7 @@ def llm_gen_ai_usage_cache_read_input_tokens( def llm_gen_ai_prompt(params: LLMAttributesParams) -> ExtractorResponse: - # a content is a message + # a part is a message messages: list[dict] = [] for content in params.llm_request.contents: @@ -119,8 +118,10 @@ def llm_gen_ai_prompt(params: LLMAttributesParams) -> ExtractorResponse: # function response if part.function_response: message[f"gen_ai.prompt.{idx}.role"] = content.role - message[f"gen_ai.prompt.{idx}.content"] = str( - content.parts[0].function_response + message[f"gen_ai.prompt.{idx}.content"] = ( + str(content.parts[0].function_response.response) + if content.parts[0].function_response + else "" ) # function call if part.function_call: From fc9af7fe3b247dcedad72f24af9374067ee11faa Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Mon, 25 Aug 2025 13:39:54 +0800 Subject: [PATCH 08/20] update attributes --- .../extractors/common_attributes_extractors.py | 13 +++++++++---- veadk/tracing/telemetry/telemetry.py | 14 +++++++++++++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py index 40d4646b..ea668998 100644 --- a/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py @@ -2,7 +2,9 @@ def common_gen_ai_system(**kwargs) -> str: - return "veadk" + """This field will be parsed as `model_provider` in Volcengine CozeLoop platform.""" + model_provider = kwargs.get("model_provider") + return model_provider or "" def common_gen_ai_system_version(**kwargs) -> str: @@ -33,7 +35,10 @@ def common_gen_ai_session_id(**kwargs) -> str: "gen_ai.system": common_gen_ai_system, "gen_ai.system.version": common_gen_ai_system_version, "gen_ai.agent.name": common_gen_ai_agent_name, - "gen_ai.app.name": common_gen_ai_app_name, - "gen_ai.user.id": common_gen_ai_user_id, - "gen_ai.session.id": common_gen_ai_session_id, + "gen_ai.app.name": common_gen_ai_app_name, # APMPlus required + "gen_ai.user.id": common_gen_ai_user_id, # APMPlus required + "gen_ai.session.id": common_gen_ai_session_id, # APMPlus required + "app.name": common_gen_ai_app_name, # CozeLoop required + "user.id": common_gen_ai_user_id, # CozeLoop required + "session.id": common_gen_ai_session_id, # CozeLoop required } diff --git a/veadk/tracing/telemetry/telemetry.py b/veadk/tracing/telemetry/telemetry.py index 7cf2388f..5cb7eb47 100644 --- a/veadk/tracing/telemetry/telemetry.py +++ b/veadk/tracing/telemetry/telemetry.py @@ -37,6 +37,10 @@ def set_common_attributes(current_span: _Span, **kwargs) -> None: common_attributes = ATTRIBUTES.get("common", {}) for span in spans_in_current_trace: + if span.name.startswith("invocation"): + span.set_attribute("gen_ai.operation.name", "chain") + elif span.name.startswith("agent_run"): + span.set_attribute("gen_ai.operation.name", "agent") for attr_name, attr_extractor in common_attributes.items(): value = attr_extractor(**kwargs) span.set_attribute(attr_name, value) @@ -65,12 +69,20 @@ def trace_call_llm( ) -> None: span = trace.get_current_span() + from veadk.agent import Agent + set_common_attributes( current_span=span, # type: ignore agent_name=invocation_context.agent.name, - app_name=invocation_context.app_name, user_id=invocation_context.user_id, + app_name=invocation_context.app_name, session_id=invocation_context.session.id, + model_provider=invocation_context.agent.model_provider + if isinstance(invocation_context.agent, Agent) + else "", + model_name=invocation_context.agent.model_name + if isinstance(invocation_context.agent, Agent) + else "", ) llm_attributes_mapping = ATTRIBUTES.get("llm", {}) From 24c7f82dbfe9c68c25e321e713802b91972457d6 Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Mon, 25 Aug 2025 14:26:21 +0800 Subject: [PATCH 09/20] fix bugs --- .../attributes/extractors/llm_attributes_extractors.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py index 0e96d719..78ca8a86 100644 --- a/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py @@ -119,8 +119,8 @@ def llm_gen_ai_prompt(params: LLMAttributesParams) -> ExtractorResponse: if part.function_response: message[f"gen_ai.prompt.{idx}.role"] = content.role message[f"gen_ai.prompt.{idx}.content"] = ( - str(content.parts[0].function_response.response) - if content.parts[0].function_response + str(part.function_response.response) + if part.function_response else "" ) # function call @@ -236,7 +236,7 @@ def llm_gen_ai_user_message(params: LLMAttributesParams) -> ExtractorResponse: if part.function_response: message_part[f"parts.{idx}.type"] = "function" message_part[f"parts.{idx}.content"] = str( - content.parts[0].function_response + part.function_response ) message_parts.append(message_part) From d18a011ac96281c74fbfa262a1d2e3eada1c866f Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Mon, 25 Aug 2025 16:48:10 +0800 Subject: [PATCH 10/20] update attributes --- veadk/tools/builtin_tools/lark.py | 2 +- .../common_attributes_extractors.py | 8 ++- .../extractors/llm_attributes_extractors.py | 20 +------ .../extractors/tool_attributes_extractors.py | 28 +++++++++ .../telemetry/attributes/extractors/types.py | 12 ++-- .../telemetry/exporters/inmemory_exporter.py | 6 -- veadk/tracing/telemetry/telemetry.py | 60 ++++++++++++------- 7 files changed, 84 insertions(+), 52 deletions(-) diff --git a/veadk/tools/builtin_tools/lark.py b/veadk/tools/builtin_tools/lark.py index a94acc23..406afb6f 100644 --- a/veadk/tools/builtin_tools/lark.py +++ b/veadk/tools/builtin_tools/lark.py @@ -46,7 +46,7 @@ def check_env(): ) from e -# check_env() +check_env() lark_tools = MCPToolset( connection_params=StdioServerParameters( diff --git a/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py index ea668998..1e47c43a 100644 --- a/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py @@ -31,6 +31,10 @@ def common_gen_ai_session_id(**kwargs) -> str: return session_id or "" +def common_cozeloop_report_source(**kwargs) -> str: + return "veadk" + + COMMON_ATTRIBUTES = { "gen_ai.system": common_gen_ai_system, "gen_ai.system.version": common_gen_ai_system_version, @@ -38,7 +42,9 @@ def common_gen_ai_session_id(**kwargs) -> str: "gen_ai.app.name": common_gen_ai_app_name, # APMPlus required "gen_ai.user.id": common_gen_ai_user_id, # APMPlus required "gen_ai.session.id": common_gen_ai_session_id, # APMPlus required - "app.name": common_gen_ai_app_name, # CozeLoop required + "agent_name": common_gen_ai_agent_name, # CozeLoop required + "app_name": common_gen_ai_app_name, # CozeLoop required "user.id": common_gen_ai_user_id, # CozeLoop required "session.id": common_gen_ai_session_id, # CozeLoop required + "cozeloop.report.source": common_cozeloop_report_source, # CozeLoop required } diff --git a/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py index 78ca8a86..d73f08da 100644 --- a/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py @@ -6,30 +6,12 @@ ) -def flatten(d, parent_key="", sep="."): - items = [] - if isinstance(d, dict): - for k, v in d.items(): - new_key = f"{parent_key}{sep}{k}" if parent_key else k - items.extend(flatten(v, new_key, sep=sep).items()) - elif isinstance(d, list): - if not d: # empty list - items.append((parent_key, "")) - else: - for i, v in enumerate(d): - new_key = f"{parent_key}{sep}{i}" if parent_key else str(i) - items.extend(flatten(v, new_key, sep=sep).items()) - else: - items.append((parent_key, d)) - return dict(items) - - def llm_gen_ai_request_model(params: LLMAttributesParams) -> ExtractorResponse: return ExtractorResponse(content=params.llm_request.model or "") def llm_gen_ai_request_type(params: LLMAttributesParams) -> ExtractorResponse: - return ExtractorResponse(content=type or "") + return ExtractorResponse(content="chat" or "") def llm_gen_ai_response_model(params: LLMAttributesParams) -> ExtractorResponse: diff --git a/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py index db6b9709..e0ca0c89 100644 --- a/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py @@ -1,3 +1,5 @@ +import json + from veadk.tracing.telemetry.attributes.extractors.types import ( ExtractorResponse, ToolAttributesParams, @@ -8,6 +10,32 @@ def tool_gen_ai_operation_name(params: ToolAttributesParams) -> ExtractorRespons return ExtractorResponse(content="execute_tool") +def tool_gen_ai_tool_message(params: ToolAttributesParams) -> ExtractorResponse: + tool_input = { + "id": "123", + "role": "tool", + "content": json.dumps( + { + "name": params.tool.name, + "description": params.tool.description, + "parameters": params.args, + } + ), + } + return ExtractorResponse(type="event", content=tool_input) + + +# def tool_gen_ai_tool_message(params: ToolAttributesParams) -> ExtractorResponse: +# # tool_output = { +# # "id": params.function_response_event., +# # "name": ..., +# # "response": ..., +# # } +# print(params.function_response_event) +# # return ExtractorResponse(content=json.dumps(tool_output) or "") + + TOOL_ATTRIBUTES = { "gen_ai.operation.name": tool_gen_ai_operation_name, + "gen_ai.tool.message": tool_gen_ai_tool_message, } diff --git a/veadk/tracing/telemetry/attributes/extractors/types.py b/veadk/tracing/telemetry/attributes/extractors/types.py index ea8931b6..4a04e652 100644 --- a/veadk/tracing/telemetry/attributes/extractors/types.py +++ b/veadk/tracing/telemetry/attributes/extractors/types.py @@ -12,13 +12,13 @@ @dataclass class ExtractorResponse: - content: Any + content: list | dict | None | str | int | float type: Literal["attribute", "event"] = "attribute" """Type of extractor response. - `raw`: span[...] = raw - `event`: span.add_event(...) = ... + `attribute`: span.add_attribute(attr_name, attr_value) + `event`: span.add_event(...) """ @staticmethod @@ -33,14 +33,16 @@ def update_span( for k, v in _res.items(): span.set_attribute(k, v) else: - span.set_attribute(attr_name, res) + # set anyway + span.set_attribute(attr_name, res) # type: ignore elif response.type == "event": if isinstance(response.content, dict): span.add_event(attr_name, response.content) - else: # list + elif isinstance(response.content, list): for event in response.content: span.add_event(attr_name, event) else: + # Unsupported response type, discard it. pass diff --git a/veadk/tracing/telemetry/exporters/inmemory_exporter.py b/veadk/tracing/telemetry/exporters/inmemory_exporter.py index 5e210a34..37b77acf 100644 --- a/veadk/tracing/telemetry/exporters/inmemory_exporter.py +++ b/veadk/tracing/telemetry/exporters/inmemory_exporter.py @@ -29,9 +29,6 @@ logger = get_logger(__name__) -inmemory_span_processor = None - - # ======== Adapted from Google ADK ======== class _InMemoryExporter(export.SpanExporter): def __init__(self) -> None: @@ -109,6 +106,3 @@ def __init__(self, name: str = "inmemory_exporter") -> None: self._exporter = _InMemoryExporter() self.processor = _InMemorySpanProcessor(self._exporter) - - global inmemory_span_processor - inmemory_span_processor = self.processor diff --git a/veadk/tracing/telemetry/telemetry.py b/veadk/tracing/telemetry/telemetry.py index 5cb7eb47..4d95745e 100644 --- a/veadk/tracing/telemetry/telemetry.py +++ b/veadk/tracing/telemetry/telemetry.py @@ -14,7 +14,6 @@ LLMAttributesParams, ToolAttributesParams, ) -from veadk.tracing.telemetry.exporters import inmemory_exporter from veadk.utils.logger import get_logger logger = get_logger(__name__) @@ -23,27 +22,47 @@ def trace_send_data(): ... -def set_common_attributes(current_span: _Span, **kwargs) -> None: +def set_common_attributes( + invocation_context: InvocationContext, current_span: _Span, **kwargs +) -> None: + from veadk.agent import Agent + if current_span.context: current_span_id = current_span.context.trace_id - - spans = inmemory_exporter.inmemory_span_processor.spans - - spans_in_current_trace = [ - span - for span in spans - if span.context and span.context.trace_id == current_span_id - ] - - common_attributes = ATTRIBUTES.get("common", {}) - for span in spans_in_current_trace: - if span.name.startswith("invocation"): - span.set_attribute("gen_ai.operation.name", "chain") - elif span.name.startswith("agent_run"): - span.set_attribute("gen_ai.operation.name", "agent") - for attr_name, attr_extractor in common_attributes.items(): - value = attr_extractor(**kwargs) - span.set_attribute(attr_name, value) + else: + logger.warning( + "Current span context is missing, failed to get `trace_id` to set common attributes." + ) + return + + if isinstance(invocation_context.agent, Agent): + try: + from veadk.tracing.telemetry.opentelemetry_tracer import OpentelemetryTracer + + tracer: OpentelemetryTracer = invocation_context.agent.tracers[0] # type: ignore + spans = tracer._inmemory_exporter.processor.spans # # type: ignore + + spans_in_current_trace = [ + span + for span in spans + if span.context and span.context.trace_id == current_span_id + ] + + common_attributes = ATTRIBUTES.get("common", {}) + for span in spans_in_current_trace: + if span.name.startswith("invocation"): + span.set_attribute("gen_ai.operation.name", "chain") + elif span.name.startswith("agent_run"): + span.set_attribute("gen_ai.operation.name", "agent") + for attr_name, attr_extractor in common_attributes.items(): + value = attr_extractor(**kwargs) + span.set_attribute(attr_name, value) + except Exception as e: + logger.error(f"Failed to set common attributes for spans: {e}") + else: + logger.warning( + "Failed to set common attributes for spans as your agent is not VeADK Agent. Skip this." + ) def trace_tool_call( @@ -72,6 +91,7 @@ def trace_call_llm( from veadk.agent import Agent set_common_attributes( + invocation_context=invocation_context, current_span=span, # type: ignore agent_name=invocation_context.agent.name, user_id=invocation_context.user_id, From c5bcd439c9803bd0b594bba54c1eab9cccc66d42 Mon Sep 17 00:00:00 2001 From: "hanzhi.421" Date: Mon, 25 Aug 2025 20:19:25 +0800 Subject: [PATCH 11/20] fix: fix apmplus metrics --- .../telemetry/exporters/apmplus_exporter.py | 34 ++++++++++--- .../metrics/opentelemetry_metrics.py | 49 ++++++------------- veadk/tracing/telemetry/telemetry.py | 10 +++- 3 files changed, 51 insertions(+), 42 deletions(-) diff --git a/veadk/tracing/telemetry/exporters/apmplus_exporter.py b/veadk/tracing/telemetry/exporters/apmplus_exporter.py index 76d44c19..39ae9072 100644 --- a/veadk/tracing/telemetry/exporters/apmplus_exporter.py +++ b/veadk/tracing/telemetry/exporters/apmplus_exporter.py @@ -14,10 +14,10 @@ from typing import Any -from opentelemetry import metrics +from opentelemetry import metrics as metrics_api from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter -from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk import metrics as metrics_sdk from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace.export import BatchSpanProcessor @@ -26,6 +26,9 @@ from veadk.config import getenv from veadk.tracing.telemetry.exporters.base_exporter import BaseExporter +from veadk.tracing.telemetry.metrics.opentelemetry_metrics import MeterUploader + +# from veadk.tracing.telemetry.metrics.opentelemetry_metrics import meter_uploader_manager from veadk.utils.logger import get_logger logger = get_logger(__name__) @@ -68,15 +71,32 @@ def model_post_init(self, context: Any) -> None: endpoint=self.config.endpoint, insecure=True, headers=self.headers ) self.processor = BatchSpanProcessor(self._exporter) + self.meter_uploader = self._init_meter_uploader(exporter_id="apmplus") + def _init_meter_uploader(self, exporter_id: str) -> MeterUploader: # init meter - resource = Resource.create() - exporter = OTLPMetricExporter(endpoint=self.config.endpoint, headers=headers) + exporter = OTLPMetricExporter( + endpoint=self.config.endpoint, headers=self.headers + ) metric_reader = PeriodicExportingMetricReader(exporter) - provider = MeterProvider(metric_readers=[metric_reader], resource=resource) - metrics.set_meter_provider(provider) - # metrics.get_meter("veadk.apmplus.meter") + global_metrics_provider = metrics_api.get_meter_provider() + + if getattr(global_metrics_provider, "_sdk_config", None): + global_resource = getattr(global_metrics_provider, "_sdk_config").resource + else: + global_resource = Resource.create() + + new_resource = Resource.create(self.resource_attributes) + merged_resource = global_resource.merge(new_resource) + + provider = metrics_sdk.MeterProvider( + metric_readers=[metric_reader], resource=merged_resource + ) + metrics_api.set_meter_provider(provider) + + meter_uploader = MeterUploader(exporter_id=exporter_id) + return meter_uploader @override def export(self) -> None: diff --git a/veadk/tracing/telemetry/metrics/opentelemetry_metrics.py b/veadk/tracing/telemetry/metrics/opentelemetry_metrics.py index be8eaa7b..ddf1dc93 100644 --- a/veadk/tracing/telemetry/metrics/opentelemetry_metrics.py +++ b/veadk/tracing/telemetry/metrics/opentelemetry_metrics.py @@ -11,33 +11,20 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -import time - +from google.adk.models import LlmResponse +from opentelemetry import metrics from opentelemetry.metrics._internal import Meter -from opentelemetry.sdk.metrics import MeterProvider -from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader from veadk.config import getenv - -class MeterContext: - def __init__( - self, - meter: Meter, - provider: MeterProvider, - reader: PeriodicExportingMetricReader, - ): - self.meter = meter - self.provider = provider - self.reader = reader +METER_NAME_TEMPLATE = "veadk.{exporter_id}.meter" class MeterUploader: - def __init__(self, meter_context: MeterContext): - self.meter = meter_context.meter - self.provider = meter_context.provider - self.reader = meter_context.reader + def __init__(self, exporter_id: str): + self.meter: Meter = metrics.get_meter( + METER_NAME_TEMPLATE.format(exporter_id=exporter_id) + ) self.base_attributes = { "gen_ai_system": "volcengine", @@ -57,17 +44,11 @@ def __init__(self, meter_context: MeterContext): unit="count", ) - def record(self, prompt_tokens: list[int], completion_tokens: list[int]): - self.llm_invoke_counter.add(len(completion_tokens), self.base_attributes) - - for prompt_token in prompt_tokens: - token_attributes = {**self.base_attributes, "gen_ai_token_type": "input"} - self.token_usage.record(prompt_token, attributes=token_attributes) - for completion_token in completion_tokens: - token_attributes = {**self.base_attributes, "gen_ai_token_type": "output"} - self.token_usage.record(completion_token, attributes=token_attributes) - - def close(self): - time.sleep(0.05) - self.reader.force_flush() - self.provider.shutdown() + def record(self, llm_response: LlmResponse): + input_token = llm_response.usage_metadata.prompt_token_count + output_token = llm_response.usage_metadata.candidates_token_count + self.llm_invoke_counter.add(1, self.base_attributes) + token_attributes = {**self.base_attributes, "gen_ai_token_type": "input"} + self.token_usage.record(input_token, attributes=token_attributes) + token_attributes = {**self.base_attributes, "gen_ai_token_type": "output"} + self.token_usage.record(output_token, attributes=token_attributes) diff --git a/veadk/tracing/telemetry/telemetry.py b/veadk/tracing/telemetry/telemetry.py index 4d95745e..c1d801fe 100644 --- a/veadk/tracing/telemetry/telemetry.py +++ b/veadk/tracing/telemetry/telemetry.py @@ -19,7 +19,12 @@ logger = get_logger(__name__) -def trace_send_data(): ... +def trace_send_data(invocation_context: InvocationContext, llm_response: LlmResponse): + tracers = invocation_context.agent.tracers + for tracer in tracers: + for exporter in getattr(tracer, "exporters", []): + if getattr(exporter, "meter_uploader", None): + exporter.meter_uploader.record(llm_response) def set_common_attributes( @@ -116,3 +121,6 @@ def trace_call_llm( for attr_name, attr_extractor in llm_attributes_mapping.items(): response: ExtractorResponse = attr_extractor(params) ExtractorResponse.update_span(span, attr_name, response) + + # Report meter + trace_send_data(invocation_context, llm_response) From 5219b4269c893c8604acc5855aaac55d9f1294b8 Mon Sep 17 00:00:00 2001 From: "hanzhi.421" Date: Mon, 25 Aug 2025 20:43:09 +0800 Subject: [PATCH 12/20] fix: remove do hooks of tracer --- veadk/agents/loop_agent.py | 15 --------------- veadk/agents/parallel_agent.py | 13 ------------- veadk/agents/sequential_agent.py | 15 --------------- .../{{cookiecutter.local_dir_name}}/src/app.py | 1 - 4 files changed, 44 deletions(-) diff --git a/veadk/agents/loop_agent.py b/veadk/agents/loop_agent.py index b7bbde3c..33d2494e 100644 --- a/veadk/agents/loop_agent.py +++ b/veadk/agents/loop_agent.py @@ -19,7 +19,6 @@ from pydantic import ConfigDict, Field from typing_extensions import Any -from veadk.agent import Agent from veadk.prompts.agent_default_prompt import DEFAULT_DESCRIPTION, DEFAULT_INSTRUCTION from veadk.tracing.base_tracer import BaseTracer from veadk.utils.logger import get_logger @@ -50,21 +49,7 @@ class LoopAgent(GoogleADKLoopAgent): tracers: list[BaseTracer] = [] """The tracers provided to agent.""" - def set_sub_agents_tracer(self, tracer) -> None: - from veadk.agents.parallel_agent import ParallelAgent - from veadk.agents.sequential_agent import SequentialAgent - - for sub_agent in self.sub_agents: - if isinstance(sub_agent, Agent): - tracer.do_hooks(sub_agent) - elif isinstance(sub_agent, (SequentialAgent, LoopAgent, ParallelAgent)): - sub_agent.set_sub_agents_tracer(tracer) - def model_post_init(self, __context: Any) -> None: super().model_post_init(None) # for sub_agents init - if self.tracers: - for tracer in self.tracers: - self.set_sub_agents_tracer(tracer) - logger.info(f"{self.__class__.__name__} `{self.name}` init done.") diff --git a/veadk/agents/parallel_agent.py b/veadk/agents/parallel_agent.py index 486c138a..d13b3a95 100644 --- a/veadk/agents/parallel_agent.py +++ b/veadk/agents/parallel_agent.py @@ -19,7 +19,6 @@ from pydantic import ConfigDict, Field from typing_extensions import Any -from veadk.agent import Agent from veadk.prompts.agent_default_prompt import DEFAULT_DESCRIPTION, DEFAULT_INSTRUCTION from veadk.tracing.base_tracer import BaseTracer from veadk.utils.logger import get_logger @@ -50,16 +49,6 @@ class ParallelAgent(GoogleADKParallelAgent): tracers: list[BaseTracer] = [] """The tracers provided to agent.""" - def set_sub_agents_tracer(self, tracer) -> None: - from veadk.agents.loop_agent import LoopAgent - from veadk.agents.sequential_agent import SequentialAgent - - for sub_agent in self.sub_agents: - if isinstance(sub_agent, Agent): - tracer.do_hooks(sub_agent) - elif isinstance(sub_agent, (SequentialAgent, LoopAgent, ParallelAgent)): - sub_agent.set_sub_agents_tracer(tracer) - def model_post_init(self, __context: Any) -> None: super().model_post_init(None) # for sub_agents init @@ -67,7 +56,5 @@ def model_post_init(self, __context: Any) -> None: logger.warning( "Enable tracing in ParallelAgent may cause OpenTelemetry context error. Issue see https://github.com/google/adk-python/issues/1670" ) - for tracer in self.tracers: - self.set_sub_agents_tracer(tracer) logger.info(f"{self.__class__.__name__} `{self.name}` init done.") diff --git a/veadk/agents/sequential_agent.py b/veadk/agents/sequential_agent.py index b7ac4b1a..cf4f5146 100644 --- a/veadk/agents/sequential_agent.py +++ b/veadk/agents/sequential_agent.py @@ -19,7 +19,6 @@ from pydantic import ConfigDict, Field from typing_extensions import Any -from veadk.agent import Agent from veadk.prompts.agent_default_prompt import DEFAULT_DESCRIPTION, DEFAULT_INSTRUCTION from veadk.tracing.base_tracer import BaseTracer from veadk.utils.logger import get_logger @@ -50,21 +49,7 @@ class SequentialAgent(GoogleADKSequentialAgent): tracers: list[BaseTracer] = [] """The tracers provided to agent.""" - def set_sub_agents_tracer(self, tracer) -> None: - from veadk.agents.loop_agent import LoopAgent - from veadk.agents.parallel_agent import ParallelAgent - - for sub_agent in self.sub_agents: - if isinstance(sub_agent, Agent): - tracer.do_hooks(sub_agent) - elif isinstance(sub_agent, (SequentialAgent, LoopAgent, ParallelAgent)): - sub_agent.set_sub_agents_tracer(tracer) - def model_post_init(self, __context: Any) -> None: super().model_post_init(None) # for sub_agents init - if self.tracers: - for tracer in self.tracers: - self.set_sub_agents_tracer(tracer) - logger.info(f"{self.__class__.__name__} `{self.name}` init done.") diff --git a/veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/src/app.py b/veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/src/app.py index 4841680d..83b37fe4 100644 --- a/veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/src/app.py +++ b/veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/src/app.py @@ -74,7 +74,6 @@ def load_tracer() -> None: name="veadk_tracer", app_name=agent_run_config.app_name, exporters=exporters ) agent_run_config.agent.tracers.extend([tracer]) - tracer.do_hooks(agent=agent_run_config.agent) def build_mcp_run_agent_func() -> Callable: From c3c61d9dc84a660ea76ac0f5cdb28b94dc001069 Mon Sep 17 00:00:00 2001 From: "hanzhi.421" Date: Mon, 25 Aug 2025 20:58:46 +0800 Subject: [PATCH 13/20] fix: add license header --- veadk/tracing/telemetry/attributes/attributes.py | 14 ++++++++++++++ .../extractors/common_attributes_extractors.py | 14 ++++++++++++++ .../extractors/llm_attributes_extractors.py | 14 ++++++++++++++ .../extractors/tool_attributes_extractors.py | 14 ++++++++++++++ .../telemetry/attributes/extractors/types.py | 14 ++++++++++++++ veadk/tracing/telemetry/telemetry.py | 14 ++++++++++++++ 6 files changed, 84 insertions(+) diff --git a/veadk/tracing/telemetry/attributes/attributes.py b/veadk/tracing/telemetry/attributes/attributes.py index 68d82ded..19ced9d9 100644 --- a/veadk/tracing/telemetry/attributes/attributes.py +++ b/veadk/tracing/telemetry/attributes/attributes.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from veadk.tracing.telemetry.attributes.extractors.common_attributes_extractors import ( COMMON_ATTRIBUTES, ) diff --git a/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py index 1e47c43a..25f05d26 100644 --- a/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from veadk.version import VERSION diff --git a/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py index d73f08da..3549e9a9 100644 --- a/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import json from veadk.tracing.telemetry.attributes.extractors.types import ( diff --git a/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py index e0ca0c89..018e6eba 100644 --- a/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import json from veadk.tracing.telemetry.attributes.extractors.types import ( diff --git a/veadk/tracing/telemetry/attributes/extractors/types.py b/veadk/tracing/telemetry/attributes/extractors/types.py index 4a04e652..7852f41d 100644 --- a/veadk/tracing/telemetry/attributes/extractors/types.py +++ b/veadk/tracing/telemetry/attributes/extractors/types.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from typing import Any, Literal from attr import dataclass diff --git a/veadk/tracing/telemetry/telemetry.py b/veadk/tracing/telemetry/telemetry.py index c1d801fe..27221fd7 100644 --- a/veadk/tracing/telemetry/telemetry.py +++ b/veadk/tracing/telemetry/telemetry.py @@ -1,3 +1,17 @@ +# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + from typing import Any from google.adk.agents.invocation_context import InvocationContext From 8876d521e6aef11af181b00c1747bd7f9c5f4730 Mon Sep 17 00:00:00 2001 From: "hanzhi.421" Date: Mon, 25 Aug 2025 21:01:40 +0800 Subject: [PATCH 14/20] fix: fix function_name of metrics --- veadk/tracing/telemetry/telemetry.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/veadk/tracing/telemetry/telemetry.py b/veadk/tracing/telemetry/telemetry.py index 27221fd7..687f0eb1 100644 --- a/veadk/tracing/telemetry/telemetry.py +++ b/veadk/tracing/telemetry/telemetry.py @@ -33,7 +33,7 @@ logger = get_logger(__name__) -def trace_send_data(invocation_context: InvocationContext, llm_response: LlmResponse): +def upload_metrics(invocation_context: InvocationContext, llm_response: LlmResponse): tracers = invocation_context.agent.tracers for tracer in tracers: for exporter in getattr(tracer, "exporters", []): @@ -41,6 +41,9 @@ def trace_send_data(invocation_context: InvocationContext, llm_response: LlmResp exporter.meter_uploader.record(llm_response) +def trace_send_data(): ... + + def set_common_attributes( invocation_context: InvocationContext, current_span: _Span, **kwargs ) -> None: @@ -137,4 +140,4 @@ def trace_call_llm( ExtractorResponse.update_span(span, attr_name, response) # Report meter - trace_send_data(invocation_context, llm_response) + upload_metrics(invocation_context, llm_response) From 9c575fb2b97d710a701ec0def9a8313ab9e76a55 Mon Sep 17 00:00:00 2001 From: "hanzhi.421" Date: Mon, 25 Aug 2025 21:37:34 +0800 Subject: [PATCH 15/20] fix: rewrite init meter uploader --- .../telemetry/exporters/apmplus_exporter.py | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/veadk/tracing/telemetry/exporters/apmplus_exporter.py b/veadk/tracing/telemetry/exporters/apmplus_exporter.py index 39ae9072..cfce5752 100644 --- a/veadk/tracing/telemetry/exporters/apmplus_exporter.py +++ b/veadk/tracing/telemetry/exporters/apmplus_exporter.py @@ -27,8 +27,6 @@ from veadk.config import getenv from veadk.tracing.telemetry.exporters.base_exporter import BaseExporter from veadk.tracing.telemetry.metrics.opentelemetry_metrics import MeterUploader - -# from veadk.tracing.telemetry.metrics.opentelemetry_metrics import meter_uploader_manager from veadk.utils.logger import get_logger logger = get_logger(__name__) @@ -74,29 +72,22 @@ def model_post_init(self, context: Any) -> None: self.meter_uploader = self._init_meter_uploader(exporter_id="apmplus") def _init_meter_uploader(self, exporter_id: str) -> MeterUploader: - # init meter - exporter = OTLPMetricExporter( - endpoint=self.config.endpoint, headers=self.headers - ) - metric_reader = PeriodicExportingMetricReader(exporter) - global_metrics_provider = metrics_api.get_meter_provider() - - if getattr(global_metrics_provider, "_sdk_config", None): - global_resource = getattr(global_metrics_provider, "_sdk_config").resource + if hasattr(global_metrics_provider, "_sdk_config"): + global_resource = global_metrics_provider._sdk_config.resource else: global_resource = Resource.create() + resource = global_resource.merge(Resource.create(self.resource_attributes)) - new_resource = Resource.create(self.resource_attributes) - merged_resource = global_resource.merge(new_resource) - - provider = metrics_sdk.MeterProvider( - metric_readers=[metric_reader], resource=merged_resource + exporter = OTLPMetricExporter( + endpoint=self.config.endpoint, headers=self.headers ) - metrics_api.set_meter_provider(provider) - - meter_uploader = MeterUploader(exporter_id=exporter_id) - return meter_uploader + metric_reader = PeriodicExportingMetricReader(exporter) + # set provider + metrics_api.set_meter_provider( + metrics_sdk.MeterProvider(metric_readers=[metric_reader], resource=resource) + ) + return MeterUploader(exporter_id=exporter_id) @override def export(self) -> None: From 8fdae92df22dd3dbaae1e7b1bfe801fdc4638085 Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Tue, 26 Aug 2025 09:59:09 +0800 Subject: [PATCH 16/20] update attributes --- .../src/app.py | 4 +-- .../extractors/llm_attributes_extractors.py | 8 ++++++ .../extractors/tool_attributes_extractors.py | 28 +++++++++++++------ .../telemetry/exporters/apmplus_exporter.py | 2 +- .../metrics/opentelemetry_metrics.py | 3 +- veadk/tracing/telemetry/telemetry.py | 18 +++++++----- 6 files changed, 42 insertions(+), 21 deletions(-) diff --git a/veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/src/app.py b/veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/src/app.py index 83b37fe4..27d2000e 100644 --- a/veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/src/app.py +++ b/veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/src/app.py @@ -70,9 +70,7 @@ def load_tracer() -> None: else: exporters.append(exporter_cls()) - tracer = OpentelemetryTracer( - name="veadk_tracer", app_name=agent_run_config.app_name, exporters=exporters - ) + tracer = OpentelemetryTracer(name="veadk_tracer", exporters=exporters) agent_run_config.agent.tracers.extend([tracer]) diff --git a/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py index 3549e9a9..df603100 100644 --- a/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py @@ -13,6 +13,7 @@ # limitations under the License. import json +from xml.etree.ElementTree import VERSION from veadk.tracing.telemetry.attributes.extractors.types import ( ExtractorResponse, @@ -359,6 +360,12 @@ def llm_gen_ai_choice(params: LLMAttributesParams) -> ExtractorResponse: return ExtractorResponse(type="event", content=message) +def llm_openinference_instrumentation_veadk( + params: LLMAttributesParams, +) -> ExtractorResponse: + return ExtractorResponse(content=VERSION) + + LLM_ATTRIBUTES = { "gen_ai.request.model": llm_gen_ai_request_model, "gen_ai.request.type": llm_gen_ai_request_type, @@ -381,4 +388,5 @@ def llm_gen_ai_choice(params: LLMAttributesParams) -> ExtractorResponse: "gen_ai.user.message": llm_gen_ai_user_message, "gen_ai.assistant.message": llm_gen_ai_assistant_message, "gen_ai.choice": llm_gen_ai_choice, + "openinference.instrumentation.veadk": llm_openinference_instrumentation_veadk, } diff --git a/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py index 018e6eba..99a8d880 100644 --- a/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py @@ -39,17 +39,27 @@ def tool_gen_ai_tool_message(params: ToolAttributesParams) -> ExtractorResponse: return ExtractorResponse(type="event", content=tool_input) -# def tool_gen_ai_tool_message(params: ToolAttributesParams) -> ExtractorResponse: -# # tool_output = { -# # "id": params.function_response_event., -# # "name": ..., -# # "response": ..., -# # } -# print(params.function_response_event) -# # return ExtractorResponse(content=json.dumps(tool_output) or "") +def tool_cozeloop_input(params: ToolAttributesParams) -> ExtractorResponse: + tool_input = { + "name": params.tool.name, + "description": params.tool.description, + "parameters": params.args, + } + return ExtractorResponse(content=json.dumps(tool_input) or "") + + +def tool_cozeloop_output(params: ToolAttributesParams) -> ExtractorResponse: + function_response = params.function_response_event.get_function_responses()[0] + tool_output = { + "id": function_response.id, + "name": function_response.name, + "response": function_response.response, + } + return ExtractorResponse(content=json.dumps(tool_output) or "") TOOL_ATTRIBUTES = { "gen_ai.operation.name": tool_gen_ai_operation_name, - "gen_ai.tool.message": tool_gen_ai_tool_message, + "cozeloop.input": tool_cozeloop_input, # CozeLoop required + "cozeloop.output": tool_cozeloop_output, # CozeLoop required } diff --git a/veadk/tracing/telemetry/exporters/apmplus_exporter.py b/veadk/tracing/telemetry/exporters/apmplus_exporter.py index cfce5752..d724f84a 100644 --- a/veadk/tracing/telemetry/exporters/apmplus_exporter.py +++ b/veadk/tracing/telemetry/exporters/apmplus_exporter.py @@ -83,7 +83,7 @@ def _init_meter_uploader(self, exporter_id: str) -> MeterUploader: endpoint=self.config.endpoint, headers=self.headers ) metric_reader = PeriodicExportingMetricReader(exporter) - # set provider + metrics_api.set_meter_provider( metrics_sdk.MeterProvider(metric_readers=[metric_reader], resource=resource) ) diff --git a/veadk/tracing/telemetry/metrics/opentelemetry_metrics.py b/veadk/tracing/telemetry/metrics/opentelemetry_metrics.py index ddf1dc93..f6f2e2c3 100644 --- a/veadk/tracing/telemetry/metrics/opentelemetry_metrics.py +++ b/veadk/tracing/telemetry/metrics/opentelemetry_metrics.py @@ -11,7 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from google.adk.models import LlmResponse + +from google.adk.models.llm_response import LlmResponse from opentelemetry import metrics from opentelemetry.metrics._internal import Meter diff --git a/veadk/tracing/telemetry/telemetry.py b/veadk/tracing/telemetry/telemetry.py index 687f0eb1..47c9061b 100644 --- a/veadk/tracing/telemetry/telemetry.py +++ b/veadk/tracing/telemetry/telemetry.py @@ -33,12 +33,17 @@ logger = get_logger(__name__) -def upload_metrics(invocation_context: InvocationContext, llm_response: LlmResponse): - tracers = invocation_context.agent.tracers - for tracer in tracers: - for exporter in getattr(tracer, "exporters", []): - if getattr(exporter, "meter_uploader", None): - exporter.meter_uploader.record(llm_response) +def upload_metrics( + invocation_context: InvocationContext, llm_response: LlmResponse +) -> None: + from veadk.agent import Agent + + if isinstance(invocation_context.agent, Agent): + tracers = invocation_context.agent.tracers + for tracer in tracers: + for exporter in getattr(tracer, "exporters", []): + if getattr(exporter, "meter_uploader", None): + exporter.meter_uploader.record(llm_response) def trace_send_data(): ... @@ -139,5 +144,4 @@ def trace_call_llm( response: ExtractorResponse = attr_extractor(params) ExtractorResponse.update_span(span, attr_name, response) - # Report meter upload_metrics(invocation_context, llm_response) From 4ee759916816ecd07d3e97d6b370b001af6074ab Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Tue, 26 Aug 2025 10:22:07 +0800 Subject: [PATCH 17/20] refine meter uploader --- .../telemetry/exporters/apmplus_exporter.py | 92 +++++++++++++++---- veadk/tracing/telemetry/metrics/__init__.py | 13 --- .../metrics/opentelemetry_metrics.py | 55 ----------- veadk/tracing/telemetry/telemetry.py | 8 +- 4 files changed, 79 insertions(+), 89 deletions(-) delete mode 100644 veadk/tracing/telemetry/metrics/__init__.py delete mode 100644 veadk/tracing/telemetry/metrics/opentelemetry_metrics.py diff --git a/veadk/tracing/telemetry/exporters/apmplus_exporter.py b/veadk/tracing/telemetry/exporters/apmplus_exporter.py index d724f84a..32c53d94 100644 --- a/veadk/tracing/telemetry/exporters/apmplus_exporter.py +++ b/veadk/tracing/telemetry/exporters/apmplus_exporter.py @@ -14,9 +14,13 @@ from typing import Any +from google.adk.models.llm_request import LlmRequest +from google.adk.models.llm_response import LlmResponse +from opentelemetry import metrics from opentelemetry import metrics as metrics_api from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from opentelemetry.metrics._internal import Meter from opentelemetry.sdk import metrics as metrics_sdk from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader from opentelemetry.sdk.resources import Resource @@ -26,12 +30,76 @@ from veadk.config import getenv from veadk.tracing.telemetry.exporters.base_exporter import BaseExporter -from veadk.tracing.telemetry.metrics.opentelemetry_metrics import MeterUploader from veadk.utils.logger import get_logger logger = get_logger(__name__) +class MeterUploader: + def __init__( + self, name: str, endpoint: str, headers: dict, resource_attributes: dict + ) -> None: + # global_metrics_provider -> global_tracer_provider + # exporter -> exporter + # metric_reader -> processor + global_metrics_provider = metrics_api.get_meter_provider() + + # 1. init resource + if hasattr(global_metrics_provider, "_sdk_config"): + global_resource = global_metrics_provider._sdk_config.resource # type: ignore + else: + global_resource = Resource.create() + + resource = global_resource.merge(Resource.create(resource_attributes)) + + # 2. init exporter and reader + exporter = OTLPMetricExporter(endpoint=endpoint, headers=headers) + metric_reader = PeriodicExportingMetricReader(exporter) + + metrics_api.set_meter_provider( + metrics_sdk.MeterProvider(metric_readers=[metric_reader], resource=resource) + ) + + # 3. init meter + self.meter: Meter = metrics.get_meter(name=name) + + # create meter attributes + self.llm_invoke_counter = self.meter.create_counter( + name="gen_ai.chat.count", + description="Number of LLM invocations", + unit="count", + ) + self.token_usage = self.meter.create_histogram( + name="gen_ai.client.token.usage", + description="Token consumption of LLM invocations", + unit="count", + ) + + def record(self, llm_request: LlmRequest, llm_response: LlmResponse) -> None: + attributes = { + "gen_ai_system": "volcengine", + "gen_ai_response_model": llm_request.model, + "gen_ai_operation_name": "chat_completions", + "stream": "false", + "server_address": "api.volcengine.com", + } # required by Volcengine APMPlus + + if llm_response.usage_metadata: + # llm invocation number += 1 + self.llm_invoke_counter.add(1, attributes) + + # upload token usage + input_token = llm_response.usage_metadata.prompt_token_count + output_token = llm_response.usage_metadata.candidates_token_count + + if input_token: + token_attributes = {**attributes, "gen_ai_token_type": "input"} + self.token_usage.record(input_token, attributes=token_attributes) + if output_token: + token_attributes = {**attributes, "gen_ai_token_type": "output"} + self.token_usage.record(output_token, attributes=token_attributes) + + class APMPlusExporterConfig(BaseModel): endpoint: str = Field( default_factory=lambda: getenv( @@ -69,25 +137,13 @@ def model_post_init(self, context: Any) -> None: endpoint=self.config.endpoint, insecure=True, headers=self.headers ) self.processor = BatchSpanProcessor(self._exporter) - self.meter_uploader = self._init_meter_uploader(exporter_id="apmplus") - - def _init_meter_uploader(self, exporter_id: str) -> MeterUploader: - global_metrics_provider = metrics_api.get_meter_provider() - if hasattr(global_metrics_provider, "_sdk_config"): - global_resource = global_metrics_provider._sdk_config.resource - else: - global_resource = Resource.create() - resource = global_resource.merge(Resource.create(self.resource_attributes)) - - exporter = OTLPMetricExporter( - endpoint=self.config.endpoint, headers=self.headers - ) - metric_reader = PeriodicExportingMetricReader(exporter) - metrics_api.set_meter_provider( - metrics_sdk.MeterProvider(metric_readers=[metric_reader], resource=resource) + self.meter_uploader = MeterUploader( + name="apmplus_meter", + endpoint=self.config.endpoint, + headers=self.headers, + resource_attributes=self.resource_attributes, ) - return MeterUploader(exporter_id=exporter_id) @override def export(self) -> None: diff --git a/veadk/tracing/telemetry/metrics/__init__.py b/veadk/tracing/telemetry/metrics/__init__.py deleted file mode 100644 index 7f463206..00000000 --- a/veadk/tracing/telemetry/metrics/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/veadk/tracing/telemetry/metrics/opentelemetry_metrics.py b/veadk/tracing/telemetry/metrics/opentelemetry_metrics.py deleted file mode 100644 index f6f2e2c3..00000000 --- a/veadk/tracing/telemetry/metrics/opentelemetry_metrics.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from google.adk.models.llm_response import LlmResponse -from opentelemetry import metrics -from opentelemetry.metrics._internal import Meter - -from veadk.config import getenv - -METER_NAME_TEMPLATE = "veadk.{exporter_id}.meter" - - -class MeterUploader: - def __init__(self, exporter_id: str): - self.meter: Meter = metrics.get_meter( - METER_NAME_TEMPLATE.format(exporter_id=exporter_id) - ) - - self.base_attributes = { - "gen_ai_system": "volcengine", - "server_address": "api.volcengine.com", - "gen_ai_response_model": getenv("MODEL_AGENT_NAME", "unknown"), - "stream": "false", - "gen_ai_operation_name": "chat_completions", - } - self.llm_invoke_counter = self.meter.create_counter( - name="gen_ai.chat.count", - description="Number of LLM invocations", - unit="count", - ) - self.token_usage = self.meter.create_histogram( - name="gen_ai.client.token.usage", - description="Token consumption of LLM invocations", - unit="count", - ) - - def record(self, llm_response: LlmResponse): - input_token = llm_response.usage_metadata.prompt_token_count - output_token = llm_response.usage_metadata.candidates_token_count - self.llm_invoke_counter.add(1, self.base_attributes) - token_attributes = {**self.base_attributes, "gen_ai_token_type": "input"} - self.token_usage.record(input_token, attributes=token_attributes) - token_attributes = {**self.base_attributes, "gen_ai_token_type": "output"} - self.token_usage.record(output_token, attributes=token_attributes) diff --git a/veadk/tracing/telemetry/telemetry.py b/veadk/tracing/telemetry/telemetry.py index 47c9061b..59dbf324 100644 --- a/veadk/tracing/telemetry/telemetry.py +++ b/veadk/tracing/telemetry/telemetry.py @@ -34,7 +34,9 @@ def upload_metrics( - invocation_context: InvocationContext, llm_response: LlmResponse + invocation_context: InvocationContext, + llm_request: LlmRequest, + llm_response: LlmResponse, ) -> None: from veadk.agent import Agent @@ -43,7 +45,7 @@ def upload_metrics( for tracer in tracers: for exporter in getattr(tracer, "exporters", []): if getattr(exporter, "meter_uploader", None): - exporter.meter_uploader.record(llm_response) + exporter.meter_uploader.record(llm_request, llm_response) def trace_send_data(): ... @@ -144,4 +146,4 @@ def trace_call_llm( response: ExtractorResponse = attr_extractor(params) ExtractorResponse.update_span(span, attr_name, response) - upload_metrics(invocation_context, llm_response) + upload_metrics(invocation_context, llm_request, llm_response) From 71caa9c831c6a7bbf30c82904e4ae9f85a225adc Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Tue, 26 Aug 2025 10:38:40 +0800 Subject: [PATCH 18/20] fix attr --- .../attributes/extractors/common_attributes_extractors.py | 5 +++++ .../attributes/extractors/llm_attributes_extractors.py | 8 -------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py index 25f05d26..510e93c8 100644 --- a/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py @@ -49,10 +49,15 @@ def common_cozeloop_report_source(**kwargs) -> str: return "veadk" +def llm_openinference_instrumentation_veadk(**kwargs) -> str: + return VERSION + + COMMON_ATTRIBUTES = { "gen_ai.system": common_gen_ai_system, "gen_ai.system.version": common_gen_ai_system_version, "gen_ai.agent.name": common_gen_ai_agent_name, + "openinference.instrumentation.veadk": llm_openinference_instrumentation_veadk, "gen_ai.app.name": common_gen_ai_app_name, # APMPlus required "gen_ai.user.id": common_gen_ai_user_id, # APMPlus required "gen_ai.session.id": common_gen_ai_session_id, # APMPlus required diff --git a/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py index df603100..3549e9a9 100644 --- a/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py @@ -13,7 +13,6 @@ # limitations under the License. import json -from xml.etree.ElementTree import VERSION from veadk.tracing.telemetry.attributes.extractors.types import ( ExtractorResponse, @@ -360,12 +359,6 @@ def llm_gen_ai_choice(params: LLMAttributesParams) -> ExtractorResponse: return ExtractorResponse(type="event", content=message) -def llm_openinference_instrumentation_veadk( - params: LLMAttributesParams, -) -> ExtractorResponse: - return ExtractorResponse(content=VERSION) - - LLM_ATTRIBUTES = { "gen_ai.request.model": llm_gen_ai_request_model, "gen_ai.request.type": llm_gen_ai_request_type, @@ -388,5 +381,4 @@ def llm_openinference_instrumentation_veadk( "gen_ai.user.message": llm_gen_ai_user_message, "gen_ai.assistant.message": llm_gen_ai_assistant_message, "gen_ai.choice": llm_gen_ai_choice, - "openinference.instrumentation.veadk": llm_openinference_instrumentation_veadk, } From c07f8cd15f7adbd06af967037bfb6889e46350d5 Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Tue, 26 Aug 2025 10:50:35 +0800 Subject: [PATCH 19/20] add attributes --- .../common_attributes_extractors.py | 6 +++-- .../extractors/llm_attributes_extractors.py | 24 ++++++++++++------- .../extractors/tool_attributes_extractors.py | 5 ++++ 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py index 510e93c8..cb5b73f1 100644 --- a/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py @@ -62,8 +62,10 @@ def llm_openinference_instrumentation_veadk(**kwargs) -> str: "gen_ai.user.id": common_gen_ai_user_id, # APMPlus required "gen_ai.session.id": common_gen_ai_session_id, # APMPlus required "agent_name": common_gen_ai_agent_name, # CozeLoop required + "agent.name": common_gen_ai_agent_name, # TLS required "app_name": common_gen_ai_app_name, # CozeLoop required - "user.id": common_gen_ai_user_id, # CozeLoop required - "session.id": common_gen_ai_session_id, # CozeLoop required + "app.name": common_gen_ai_app_name, # TLS required + "user.id": common_gen_ai_user_id, # CozeLoop / TLS required + "session.id": common_gen_ai_session_id, # CozeLoop / TLS required "cozeloop.report.source": common_cozeloop_report_source, # CozeLoop required } diff --git a/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py index 3549e9a9..39828a19 100644 --- a/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py @@ -360,25 +360,33 @@ def llm_gen_ai_choice(params: LLMAttributesParams) -> ExtractorResponse: LLM_ATTRIBUTES = { + # ===== request attributes ===== "gen_ai.request.model": llm_gen_ai_request_model, "gen_ai.request.type": llm_gen_ai_request_type, - "gen_ai.response.model": llm_gen_ai_response_model, "gen_ai.request.max_tokens": llm_gen_ai_request_max_tokens, "gen_ai.request.temperature": llm_gen_ai_request_temperature, "gen_ai.request.top_p": llm_gen_ai_request_top_p, - "gen_ai.prompt": llm_gen_ai_prompt, - "gen_ai.completion": llm_gen_ai_completion, + # ===== response attributes ===== + "gen_ai.response.model": llm_gen_ai_response_model, "gen_ai.response.stop_reason": llm_gen_ai_response_stop_reason, "gen_ai.response.finish_reason": llm_gen_ai_response_finish_reason, - "gen_ai.usage.input_tokens": llm_gen_ai_usage_input_tokens, - "gen_ai.usage.output_tokens": llm_gen_ai_usage_output_tokens, - "gen_ai.usage.total_tokens": llm_gen_ai_usage_total_tokens, - "gen_ai.usage.cache_creation_input_tokens": llm_gen_ai_usage_cache_creation_input_tokens, - "gen_ai.usage.cache_read_input_tokens": llm_gen_ai_usage_cache_read_input_tokens, + # ===== streaming ===== "gen_ai.is_streaming": llm_gen_ai_is_streaming, + # ===== span type ===== "gen_ai.operation.name": llm_gen_ai_operation_name, + # ===== inputs and outputs ===== + # events "gen_ai.system.message": llm_gen_ai_system_message, "gen_ai.user.message": llm_gen_ai_user_message, "gen_ai.assistant.message": llm_gen_ai_assistant_message, "gen_ai.choice": llm_gen_ai_choice, + # attributes + "gen_ai.prompt": llm_gen_ai_prompt, + "gen_ai.completion": llm_gen_ai_completion, + # ===== usage ===== + "gen_ai.usage.input_tokens": llm_gen_ai_usage_input_tokens, + "gen_ai.usage.output_tokens": llm_gen_ai_usage_output_tokens, + "gen_ai.usage.total_tokens": llm_gen_ai_usage_total_tokens, + "gen_ai.usage.cache_creation_input_tokens": llm_gen_ai_usage_cache_creation_input_tokens, + "gen_ai.usage.cache_read_input_tokens": llm_gen_ai_usage_cache_read_input_tokens, } diff --git a/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py b/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py index 99a8d880..1ebb5d8a 100644 --- a/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py +++ b/veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py @@ -48,6 +48,10 @@ def tool_cozeloop_input(params: ToolAttributesParams) -> ExtractorResponse: return ExtractorResponse(content=json.dumps(tool_input) or "") +def tool_gen_ai_tool_name(params: ToolAttributesParams) -> ExtractorResponse: + return ExtractorResponse(content=params.tool.name or "") + + def tool_cozeloop_output(params: ToolAttributesParams) -> ExtractorResponse: function_response = params.function_response_event.get_function_responses()[0] tool_output = { @@ -60,6 +64,7 @@ def tool_cozeloop_output(params: ToolAttributesParams) -> ExtractorResponse: TOOL_ATTRIBUTES = { "gen_ai.operation.name": tool_gen_ai_operation_name, + "gen_ai.tool.name": tool_gen_ai_tool_name, # TLS required "cozeloop.input": tool_cozeloop_input, # CozeLoop required "cozeloop.output": tool_cozeloop_output, # CozeLoop required } From 8758e2f3aaf62ed29324dbae0aef2658a55d2643 Mon Sep 17 00:00:00 2001 From: "fangyaozheng@bytedance.com" Date: Tue, 26 Aug 2025 10:53:16 +0800 Subject: [PATCH 20/20] remove useless class --- veadk/tracing/base_tracer.py | 5 +++-- veadk/tracing/telemetry/exporters/cozeloop_exporter.py | 4 ---- veadk/tracing/telemetry/exporters/tls_exporter.py | 3 ++- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/veadk/tracing/base_tracer.py b/veadk/tracing/base_tracer.py index bd88bd5a..ca47b001 100644 --- a/veadk/tracing/base_tracer.py +++ b/veadk/tracing/base_tracer.py @@ -14,7 +14,6 @@ from abc import ABC, abstractmethod - from veadk.utils.logger import get_logger logger = get_logger(__name__) @@ -46,4 +45,6 @@ def __init__(self, name: str): self._trace_file_path = "" @abstractmethod - def dump(self, user_id: str, session_id: str, path: str = "/tmp") -> str: ... + def dump(self, user_id: str, session_id: str, path: str = "/tmp") -> str: + """Dump the trace data to a local file.""" + ... diff --git a/veadk/tracing/telemetry/exporters/cozeloop_exporter.py b/veadk/tracing/telemetry/exporters/cozeloop_exporter.py index beb6c7ae..ef4e4ed8 100644 --- a/veadk/tracing/telemetry/exporters/cozeloop_exporter.py +++ b/veadk/tracing/telemetry/exporters/cozeloop_exporter.py @@ -43,10 +43,6 @@ class CozeloopExporterConfig(BaseModel): ) -class _CozeloopExporter(OTLPSpanExporter): - pass - - class CozeloopExporter(BaseExporter): config: CozeloopExporterConfig = Field(default_factory=CozeloopExporterConfig) diff --git a/veadk/tracing/telemetry/exporters/tls_exporter.py b/veadk/tracing/telemetry/exporters/tls_exporter.py index dddc6fb1..29829205 100644 --- a/veadk/tracing/telemetry/exporters/tls_exporter.py +++ b/veadk/tracing/telemetry/exporters/tls_exporter.py @@ -12,10 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any + from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.trace.export import BatchSpanProcessor from pydantic import BaseModel, Field -from typing import Any from typing_extensions import override from veadk.config import getenv