"""Enterprise trace handler — duck-typed, NOT a BaseTraceInstance subclass. Invoked directly in the Celery task, not through OpsTraceManager dispatch. Only requires a matching ``trace(trace_info)`` method signature. Signal strategy: - **Traces (spans)**: workflow run, node execution, draft node execution only. - **Metrics + structured logs**: all other event types. """ from __future__ import annotations import json import logging from typing import Any, cast from opentelemetry.util.types import AttributeValue from core.ops.entities.trace_entity import ( BaseTraceInfo, DatasetRetrievalTraceInfo, DraftNodeExecutionTrace, GenerateNameTraceInfo, MessageTraceInfo, ModerationTraceInfo, OperationType, PromptGenerationTraceInfo, SuggestedQuestionTraceInfo, ToolTraceInfo, WorkflowNodeTraceInfo, WorkflowTraceInfo, ) from enterprise.telemetry.entities import ( EnterpriseTelemetryCounter, EnterpriseTelemetryHistogram, EnterpriseTelemetrySpan, ) from enterprise.telemetry.telemetry_log import emit_metric_only_event, emit_telemetry_log logger = logging.getLogger(__name__) class EnterpriseOtelTrace: """Duck-typed enterprise trace handler. ``*_trace`` methods emit spans (workflow/node only) or structured logs (all other events), plus metrics at 100 % accuracy. """ def __init__(self) -> None: from extensions.ext_enterprise_telemetry import get_enterprise_exporter exporter = get_enterprise_exporter() if exporter is None: raise RuntimeError("EnterpriseOtelTrace instantiated but exporter is not initialized") self._exporter = exporter def trace(self, trace_info: BaseTraceInfo) -> None: if isinstance(trace_info, WorkflowTraceInfo): self._workflow_trace(trace_info) elif isinstance(trace_info, MessageTraceInfo): self._message_trace(trace_info) elif isinstance(trace_info, ToolTraceInfo): self._tool_trace(trace_info) elif isinstance(trace_info, DraftNodeExecutionTrace): self._draft_node_execution_trace(trace_info) elif isinstance(trace_info, WorkflowNodeTraceInfo): self._node_execution_trace(trace_info) elif isinstance(trace_info, ModerationTraceInfo): self._moderation_trace(trace_info) elif isinstance(trace_info, SuggestedQuestionTraceInfo): self._suggested_question_trace(trace_info) elif isinstance(trace_info, DatasetRetrievalTraceInfo): self._dataset_retrieval_trace(trace_info) elif isinstance(trace_info, GenerateNameTraceInfo): self._generate_name_trace(trace_info) elif isinstance(trace_info, PromptGenerationTraceInfo): self._prompt_generation_trace(trace_info) def _common_attrs(self, trace_info: BaseTraceInfo) -> dict[str, Any]: metadata = self._metadata(trace_info) tenant_id, app_id, user_id = self._context_ids(trace_info, metadata) return { "dify.trace_id": trace_info.trace_id, "dify.tenant_id": tenant_id, "dify.app_id": app_id, "dify.app.name": metadata.get("app_name"), "dify.workspace.name": metadata.get("workspace_name"), "gen_ai.user.id": user_id, "dify.message.id": trace_info.message_id, } def _metadata(self, trace_info: BaseTraceInfo) -> dict[str, Any]: return trace_info.metadata def _context_ids( self, trace_info: BaseTraceInfo, metadata: dict[str, Any], ) -> tuple[str | None, str | None, str | None]: tenant_id = getattr(trace_info, "tenant_id", None) or metadata.get("tenant_id") app_id = getattr(trace_info, "app_id", None) or metadata.get("app_id") user_id = getattr(trace_info, "user_id", None) or metadata.get("user_id") return tenant_id, app_id, user_id def _labels(self, **values: AttributeValue) -> dict[str, AttributeValue]: return dict(values) def _safe_payload_value(self, value: Any) -> str | dict[str, Any] | list[object] | None: if isinstance(value, str): return value if isinstance(value, dict): return cast(dict[str, Any], value) if isinstance(value, list): items: list[object] = [] for item in cast(list[object], value): items.append(item) return items return None def _content_or_ref(self, value: Any, ref: str) -> Any: if self._exporter.include_content: return self._maybe_json(value) return ref def _maybe_json(self, value: Any) -> str | None: if value is None: return None if isinstance(value, str): return value try: return json.dumps(value, default=str) except (TypeError, ValueError): return str(value) # ------------------------------------------------------------------ # SPAN-emitting handlers (workflow, node execution, draft node) # ------------------------------------------------------------------ def _workflow_trace(self, info: WorkflowTraceInfo) -> None: metadata = self._metadata(info) tenant_id, app_id, user_id = self._context_ids(info, metadata) # -- Slim span attrs: identity + structure + status + timing only -- span_attrs: dict[str, Any] = { "dify.trace_id": info.trace_id, "dify.tenant_id": tenant_id, "dify.app_id": app_id, "dify.workflow.id": info.workflow_id, "dify.workflow.run_id": info.workflow_run_id, "dify.workflow.status": info.workflow_run_status, "dify.workflow.error": info.error, "dify.workflow.elapsed_time": info.workflow_run_elapsed_time, "dify.invoke_from": metadata.get("triggered_from"), "dify.conversation.id": info.conversation_id, "dify.message.id": info.message_id, "dify.invoked_by": info.invoked_by, } trace_correlation_override: str | None = None parent_span_id_source: str | None = None parent_ctx = metadata.get("parent_trace_context") if isinstance(parent_ctx, dict): parent_ctx_dict = cast(dict[str, Any], parent_ctx) span_attrs["dify.parent.trace_id"] = parent_ctx_dict.get("trace_id") span_attrs["dify.parent.node.execution_id"] = parent_ctx_dict.get("parent_node_execution_id") span_attrs["dify.parent.workflow.run_id"] = parent_ctx_dict.get("parent_workflow_run_id") span_attrs["dify.parent.app.id"] = parent_ctx_dict.get("parent_app_id") trace_override_value = parent_ctx_dict.get("parent_workflow_run_id") if isinstance(trace_override_value, str): trace_correlation_override = trace_override_value parent_span_value = parent_ctx_dict.get("parent_node_execution_id") if isinstance(parent_span_value, str): parent_span_id_source = parent_span_value self._exporter.export_span( EnterpriseTelemetrySpan.WORKFLOW_RUN, span_attrs, correlation_id=info.workflow_run_id, span_id_source=info.workflow_run_id, start_time=info.start_time, end_time=info.end_time, trace_correlation_override=trace_correlation_override, parent_span_id_source=parent_span_id_source, ) # -- Companion log: ALL attrs (span + detail) for full picture -- log_attrs: dict[str, Any] = {**span_attrs} log_attrs.update( { "dify.app.name": metadata.get("app_name"), "dify.workspace.name": metadata.get("workspace_name"), "gen_ai.user.id": user_id, "gen_ai.usage.total_tokens": info.total_tokens, "dify.workflow.version": info.workflow_run_version, } ) ref = f"ref:workflow_run_id={info.workflow_run_id}" log_attrs["dify.workflow.inputs"] = self._content_or_ref(info.workflow_run_inputs, ref) log_attrs["dify.workflow.outputs"] = self._content_or_ref(info.workflow_run_outputs, ref) log_attrs["dify.workflow.query"] = self._content_or_ref(info.query, ref) emit_telemetry_log( event_name="dify.workflow.run", attributes=log_attrs, signal="span_detail", trace_id_source=info.workflow_run_id, span_id_source=info.workflow_run_id, tenant_id=tenant_id, user_id=user_id, ) # -- Metrics -- labels = self._labels( tenant_id=tenant_id or "", app_id=app_id or "", ) token_labels = self._labels( **labels, operation_type=OperationType.WORKFLOW, ) self._exporter.increment_counter(EnterpriseTelemetryCounter.TOKENS, info.total_tokens, token_labels) if info.prompt_tokens is not None and info.prompt_tokens > 0: self._exporter.increment_counter(EnterpriseTelemetryCounter.INPUT_TOKENS, info.prompt_tokens, token_labels) if info.completion_tokens is not None and info.completion_tokens > 0: self._exporter.increment_counter( EnterpriseTelemetryCounter.OUTPUT_TOKENS, info.completion_tokens, token_labels ) invoke_from = metadata.get("triggered_from", "") self._exporter.increment_counter( EnterpriseTelemetryCounter.REQUESTS, 1, self._labels( **labels, type="workflow", status=info.workflow_run_status, invoke_from=invoke_from, ), ) self._exporter.record_histogram( EnterpriseTelemetryHistogram.WORKFLOW_DURATION, float(info.workflow_run_elapsed_time), self._labels( **labels, status=info.workflow_run_status, ), ) if info.error: self._exporter.increment_counter( EnterpriseTelemetryCounter.ERRORS, 1, self._labels( **labels, type="workflow", ), ) def _node_execution_trace(self, info: WorkflowNodeTraceInfo) -> None: self._emit_node_execution_trace(info, EnterpriseTelemetrySpan.NODE_EXECUTION, "node") def _draft_node_execution_trace(self, info: DraftNodeExecutionTrace) -> None: self._emit_node_execution_trace( info, EnterpriseTelemetrySpan.DRAFT_NODE_EXECUTION, "draft_node", correlation_id_override=info.node_execution_id, trace_correlation_override_param=info.workflow_run_id, ) def _emit_node_execution_trace( self, info: WorkflowNodeTraceInfo, span_name: EnterpriseTelemetrySpan, request_type: str, correlation_id_override: str | None = None, trace_correlation_override_param: str | None = None, ) -> None: metadata = self._metadata(info) tenant_id, app_id, user_id = self._context_ids(info, metadata) # -- Slim span attrs: identity + structure + status + timing -- span_attrs: dict[str, Any] = { "dify.trace_id": info.trace_id, "dify.tenant_id": tenant_id, "dify.app_id": app_id, "dify.workflow.id": info.workflow_id, "dify.workflow.run_id": info.workflow_run_id, "dify.message.id": info.message_id, "dify.conversation.id": metadata.get("conversation_id"), "dify.node.execution_id": info.node_execution_id, "dify.node.id": info.node_id, "dify.node.type": info.node_type, "dify.node.title": info.title, "dify.node.status": info.status, "dify.node.error": info.error, "dify.node.elapsed_time": info.elapsed_time, "dify.node.index": info.index, "dify.node.predecessor_node_id": info.predecessor_node_id, "dify.node.iteration_id": info.iteration_id, "dify.node.loop_id": info.loop_id, "dify.node.parallel_id": info.parallel_id, "dify.node.invoked_by": info.invoked_by, } trace_correlation_override = trace_correlation_override_param parent_ctx = metadata.get("parent_trace_context") if isinstance(parent_ctx, dict): parent_ctx_dict = cast(dict[str, Any], parent_ctx) override_value = parent_ctx_dict.get("parent_workflow_run_id") if isinstance(override_value, str): trace_correlation_override = override_value effective_correlation_id = correlation_id_override or info.workflow_run_id self._exporter.export_span( span_name, span_attrs, correlation_id=effective_correlation_id, span_id_source=info.node_execution_id, start_time=info.start_time, end_time=info.end_time, trace_correlation_override=trace_correlation_override, ) # -- Companion log: ALL attrs (span + detail) -- log_attrs: dict[str, Any] = {**span_attrs} log_attrs.update( { "dify.app.name": metadata.get("app_name"), "dify.workspace.name": metadata.get("workspace_name"), "dify.invoke_from": metadata.get("invoke_from"), "gen_ai.user.id": user_id, "gen_ai.usage.total_tokens": info.total_tokens, "dify.node.total_price": info.total_price, "dify.node.currency": info.currency, "gen_ai.provider.name": info.model_provider, "gen_ai.request.model": info.model_name, "gen_ai.tool.name": info.tool_name, "dify.node.iteration_index": info.iteration_index, "dify.node.loop_index": info.loop_index, "dify.plugin.name": metadata.get("plugin_name"), "dify.credential.name": metadata.get("credential_name"), "dify.dataset.ids": self._maybe_json(metadata.get("dataset_ids")), "dify.dataset.names": self._maybe_json(metadata.get("dataset_names")), } ) ref = f"ref:node_execution_id={info.node_execution_id}" log_attrs["dify.node.inputs"] = self._content_or_ref(info.node_inputs, ref) log_attrs["dify.node.outputs"] = self._content_or_ref(info.node_outputs, ref) log_attrs["dify.node.process_data"] = self._content_or_ref(info.process_data, ref) emit_telemetry_log( event_name=span_name.value, attributes=log_attrs, signal="span_detail", trace_id_source=info.workflow_run_id, span_id_source=info.node_execution_id, tenant_id=tenant_id, user_id=user_id, ) # -- Metrics -- labels = self._labels( tenant_id=tenant_id or "", app_id=app_id or "", node_type=info.node_type, model_provider=info.model_provider or "", ) if info.total_tokens: token_labels = self._labels( **labels, model_name=info.model_name or "", operation_type=OperationType.NODE_EXECUTION, ) self._exporter.increment_counter(EnterpriseTelemetryCounter.TOKENS, info.total_tokens, token_labels) if info.prompt_tokens is not None and info.prompt_tokens > 0: self._exporter.increment_counter( EnterpriseTelemetryCounter.INPUT_TOKENS, info.prompt_tokens, token_labels ) if info.completion_tokens is not None and info.completion_tokens > 0: self._exporter.increment_counter( EnterpriseTelemetryCounter.OUTPUT_TOKENS, info.completion_tokens, token_labels ) self._exporter.increment_counter( EnterpriseTelemetryCounter.REQUESTS, 1, self._labels( **labels, type=request_type, status=info.status, ), ) duration_labels = dict(labels) plugin_name = metadata.get("plugin_name") if plugin_name and info.node_type in {"tool", "knowledge-retrieval"}: duration_labels["plugin_name"] = plugin_name self._exporter.record_histogram(EnterpriseTelemetryHistogram.NODE_DURATION, info.elapsed_time, duration_labels) if info.error: self._exporter.increment_counter( EnterpriseTelemetryCounter.ERRORS, 1, self._labels( **labels, type=request_type, ), ) # ------------------------------------------------------------------ # METRIC-ONLY handlers (structured log + counters/histograms) # ------------------------------------------------------------------ def _message_trace(self, info: MessageTraceInfo) -> None: metadata = self._metadata(info) tenant_id, app_id, user_id = self._context_ids(info, metadata) attrs = self._common_attrs(info) attrs.update( { "dify.invoke_from": metadata.get("from_source"), "dify.conversation.id": metadata.get("conversation_id"), "dify.conversation.mode": info.conversation_mode, "gen_ai.provider.name": metadata.get("ls_provider"), "gen_ai.request.model": metadata.get("ls_model_name"), "gen_ai.usage.input_tokens": info.message_tokens, "gen_ai.usage.output_tokens": info.answer_tokens, "gen_ai.usage.total_tokens": info.total_tokens, "dify.message.status": metadata.get("status"), "dify.message.error": info.error, "dify.message.from_source": metadata.get("from_source"), "dify.message.from_end_user_id": metadata.get("from_end_user_id"), "dify.message.from_account_id": metadata.get("from_account_id"), "dify.streaming": info.is_streaming_request, "dify.message.time_to_first_token": info.gen_ai_server_time_to_first_token, "dify.message.streaming_duration": info.llm_streaming_time_to_generate, "dify.workflow.run_id": metadata.get("workflow_run_id"), } ) node_execution_id = metadata.get("node_execution_id") if node_execution_id: attrs["dify.node.execution_id"] = node_execution_id ref = f"ref:message_id={info.message_id}" inputs = self._safe_payload_value(info.inputs) outputs = self._safe_payload_value(info.outputs) attrs["dify.message.inputs"] = self._content_or_ref(inputs, ref) attrs["dify.message.outputs"] = self._content_or_ref(outputs, ref) emit_metric_only_event( event_name="dify.message.run", attributes=attrs, trace_id_source=metadata.get("workflow_run_id") or str(info.message_id) if info.message_id else None, span_id_source=node_execution_id, tenant_id=tenant_id, user_id=user_id, ) labels = self._labels( tenant_id=tenant_id or "", app_id=app_id or "", model_provider=metadata.get("ls_provider", ""), model_name=metadata.get("ls_model_name", ""), ) token_labels = self._labels( **labels, operation_type=OperationType.MESSAGE, ) self._exporter.increment_counter(EnterpriseTelemetryCounter.TOKENS, info.total_tokens, token_labels) if info.message_tokens > 0: self._exporter.increment_counter(EnterpriseTelemetryCounter.INPUT_TOKENS, info.message_tokens, token_labels) if info.answer_tokens > 0: self._exporter.increment_counter(EnterpriseTelemetryCounter.OUTPUT_TOKENS, info.answer_tokens, token_labels) invoke_from = metadata.get("from_source", "") self._exporter.increment_counter( EnterpriseTelemetryCounter.REQUESTS, 1, self._labels( **labels, type="message", status=metadata.get("status", ""), invoke_from=invoke_from, ), ) if info.start_time and info.end_time: duration = (info.end_time - info.start_time).total_seconds() self._exporter.record_histogram(EnterpriseTelemetryHistogram.MESSAGE_DURATION, duration, labels) if info.gen_ai_server_time_to_first_token is not None: self._exporter.record_histogram( EnterpriseTelemetryHistogram.MESSAGE_TTFT, info.gen_ai_server_time_to_first_token, labels ) if info.error: self._exporter.increment_counter( EnterpriseTelemetryCounter.ERRORS, 1, self._labels( **labels, type="message", ), ) def _tool_trace(self, info: ToolTraceInfo) -> None: metadata = self._metadata(info) tenant_id, app_id, user_id = self._context_ids(info, metadata) attrs = self._common_attrs(info) attrs.update( { "gen_ai.tool.name": info.tool_name, "dify.tool.time_cost": info.time_cost, "dify.tool.error": info.error, "dify.workflow.run_id": metadata.get("workflow_run_id"), } ) node_execution_id = metadata.get("node_execution_id") if node_execution_id: attrs["dify.node.execution_id"] = node_execution_id ref = f"ref:message_id={info.message_id}" attrs["dify.tool.inputs"] = self._content_or_ref(info.tool_inputs, ref) attrs["dify.tool.outputs"] = self._content_or_ref(info.tool_outputs, ref) attrs["dify.tool.parameters"] = self._content_or_ref(info.tool_parameters, ref) attrs["dify.tool.config"] = self._content_or_ref(info.tool_config, ref) emit_metric_only_event( event_name="dify.tool.execution", attributes=attrs, span_id_source=node_execution_id, tenant_id=tenant_id, user_id=user_id, ) labels = self._labels( tenant_id=tenant_id or "", app_id=app_id or "", tool_name=info.tool_name, ) self._exporter.increment_counter( EnterpriseTelemetryCounter.REQUESTS, 1, self._labels( **labels, type="tool", ), ) self._exporter.record_histogram(EnterpriseTelemetryHistogram.TOOL_DURATION, float(info.time_cost), labels) if info.error: self._exporter.increment_counter( EnterpriseTelemetryCounter.ERRORS, 1, self._labels( **labels, type="tool", ), ) def _moderation_trace(self, info: ModerationTraceInfo) -> None: metadata = self._metadata(info) tenant_id, app_id, user_id = self._context_ids(info, metadata) attrs = self._common_attrs(info) attrs.update( { "dify.moderation.flagged": info.flagged, "dify.moderation.action": info.action, "dify.moderation.preset_response": info.preset_response, "dify.workflow.run_id": metadata.get("workflow_run_id"), } ) node_execution_id = metadata.get("node_execution_id") if node_execution_id: attrs["dify.node.execution_id"] = node_execution_id attrs["dify.moderation.query"] = self._content_or_ref( info.query, f"ref:message_id={info.message_id}", ) emit_metric_only_event( event_name="dify.moderation.check", attributes=attrs, span_id_source=node_execution_id, tenant_id=tenant_id, user_id=user_id, ) labels = self._labels( tenant_id=tenant_id or "", app_id=app_id or "", ) self._exporter.increment_counter( EnterpriseTelemetryCounter.REQUESTS, 1, self._labels( **labels, type="moderation", ), ) def _suggested_question_trace(self, info: SuggestedQuestionTraceInfo) -> None: metadata = self._metadata(info) tenant_id, app_id, user_id = self._context_ids(info, metadata) attrs = self._common_attrs(info) attrs.update( { "gen_ai.usage.total_tokens": info.total_tokens, "dify.suggested_question.status": info.status, "dify.suggested_question.error": info.error, "gen_ai.provider.name": info.model_provider, "gen_ai.request.model": info.model_id, "dify.suggested_question.count": len(info.suggested_question), "dify.workflow.run_id": metadata.get("workflow_run_id"), } ) node_execution_id = metadata.get("node_execution_id") if node_execution_id: attrs["dify.node.execution_id"] = node_execution_id attrs["dify.suggested_question.questions"] = self._content_or_ref( info.suggested_question, f"ref:message_id={info.message_id}", ) emit_metric_only_event( event_name="dify.suggested_question.generation", attributes=attrs, span_id_source=node_execution_id, tenant_id=tenant_id, user_id=user_id, ) labels = self._labels( tenant_id=tenant_id or "", app_id=app_id or "", ) self._exporter.increment_counter( EnterpriseTelemetryCounter.REQUESTS, 1, self._labels( **labels, type="suggested_question", ), ) def _dataset_retrieval_trace(self, info: DatasetRetrievalTraceInfo) -> None: metadata = self._metadata(info) tenant_id, app_id, user_id = self._context_ids(info, metadata) attrs = self._common_attrs(info) attrs["dify.dataset.error"] = info.error attrs["dify.workflow.run_id"] = metadata.get("workflow_run_id") node_execution_id = metadata.get("node_execution_id") if node_execution_id: attrs["dify.node.execution_id"] = node_execution_id docs: list[dict[str, Any]] = [] documents_any: Any = info.documents documents_list: list[Any] = cast(list[Any], documents_any) if isinstance(documents_any, list) else [] for entry in documents_list: if isinstance(entry, dict): entry_dict: dict[str, Any] = cast(dict[str, Any], entry) docs.append(entry_dict) dataset_ids: list[str] = [] dataset_names: list[str] = [] structured_docs: list[dict[str, Any]] = [] for doc in docs: meta_raw = doc.get("metadata") meta: dict[str, Any] = cast(dict[str, Any], meta_raw) if isinstance(meta_raw, dict) else {} did = meta.get("dataset_id") dname = meta.get("dataset_name") if did and did not in dataset_ids: dataset_ids.append(did) if dname and dname not in dataset_names: dataset_names.append(dname) structured_docs.append( { "dataset_id": did, "document_id": meta.get("document_id"), "segment_id": meta.get("segment_id"), "score": meta.get("score"), } ) attrs["dify.dataset.ids"] = self._maybe_json(dataset_ids) attrs["dify.dataset.names"] = self._maybe_json(dataset_names) attrs["dify.retrieval.document_count"] = len(docs) embedding_models_raw: Any = metadata.get("embedding_models") embedding_models: dict[str, Any] = ( cast(dict[str, Any], embedding_models_raw) if isinstance(embedding_models_raw, dict) else {} ) if embedding_models: providers: list[str] = [] models: list[str] = [] for ds_info in embedding_models.values(): if isinstance(ds_info, dict): ds_info_dict: dict[str, Any] = cast(dict[str, Any], ds_info) p = ds_info_dict.get("embedding_model_provider", "") m = ds_info_dict.get("embedding_model", "") if p and p not in providers: providers.append(p) if m and m not in models: models.append(m) attrs["dify.dataset.embedding_providers"] = self._maybe_json(providers) attrs["dify.dataset.embedding_models"] = self._maybe_json(models) ref = f"ref:message_id={info.message_id}" retrieval_inputs = self._safe_payload_value(info.inputs) attrs["dify.retrieval.query"] = self._content_or_ref(retrieval_inputs, ref) attrs["dify.dataset.documents"] = self._content_or_ref(structured_docs, ref) emit_metric_only_event( event_name="dify.dataset.retrieval", attributes=attrs, trace_id_source=metadata.get("workflow_run_id") or str(info.message_id) if info.message_id else None, span_id_source=node_execution_id or (str(info.message_id) if info.message_id else None), tenant_id=tenant_id, user_id=user_id, ) labels = self._labels( tenant_id=tenant_id or "", app_id=app_id or "", ) self._exporter.increment_counter( EnterpriseTelemetryCounter.REQUESTS, 1, self._labels( **labels, type="dataset_retrieval", ), ) for did in dataset_ids: self._exporter.increment_counter( EnterpriseTelemetryCounter.DATASET_RETRIEVALS, 1, self._labels( **labels, dataset_id=did, ), ) def _generate_name_trace(self, info: GenerateNameTraceInfo) -> None: metadata = self._metadata(info) tenant_id, app_id, user_id = self._context_ids(info, metadata) attrs = self._common_attrs(info) attrs["dify.conversation.id"] = info.conversation_id node_execution_id = metadata.get("node_execution_id") if node_execution_id: attrs["dify.node.execution_id"] = node_execution_id ref = f"ref:conversation_id={info.conversation_id}" inputs = self._safe_payload_value(info.inputs) outputs = self._safe_payload_value(info.outputs) attrs["dify.generate_name.inputs"] = self._content_or_ref(inputs, ref) attrs["dify.generate_name.outputs"] = self._content_or_ref(outputs, ref) emit_metric_only_event( event_name="dify.generate_name.execution", attributes=attrs, span_id_source=node_execution_id, tenant_id=tenant_id, user_id=user_id, ) labels = self._labels( tenant_id=tenant_id or "", app_id=app_id or "", ) self._exporter.increment_counter( EnterpriseTelemetryCounter.REQUESTS, 1, self._labels( **labels, type="generate_name", ), ) def _prompt_generation_trace(self, info: PromptGenerationTraceInfo) -> None: metadata = self._metadata(info) tenant_id, app_id, user_id = self._context_ids(info, metadata) attrs = { "dify.trace_id": info.trace_id, "dify.tenant_id": tenant_id, "dify.user.id": user_id, "dify.app.id": app_id or "", "dify.app.name": metadata.get("app_name"), "dify.workspace.name": metadata.get("workspace_name"), "dify.operation.type": info.operation_type, "gen_ai.provider.name": info.model_provider, "gen_ai.request.model": info.model_name, "gen_ai.usage.input_tokens": info.prompt_tokens, "gen_ai.usage.output_tokens": info.completion_tokens, "gen_ai.usage.total_tokens": info.total_tokens, "dify.prompt_generation.latency": info.latency, "dify.prompt_generation.error": info.error, } node_execution_id = metadata.get("node_execution_id") if node_execution_id: attrs["dify.node.execution_id"] = node_execution_id if info.total_price is not None: attrs["dify.prompt_generation.total_price"] = info.total_price attrs["dify.prompt_generation.currency"] = info.currency ref = f"ref:trace_id={info.trace_id}" outputs = self._safe_payload_value(info.outputs) attrs["dify.prompt_generation.instruction"] = self._content_or_ref(info.instruction, ref) attrs["dify.prompt_generation.output"] = self._content_or_ref(outputs, ref) emit_metric_only_event( event_name="dify.prompt_generation.execution", attributes=attrs, span_id_source=node_execution_id, tenant_id=tenant_id, user_id=user_id, ) labels = self._labels( tenant_id=tenant_id or "", app_id=app_id or "", operation_type=info.operation_type, model_provider=info.model_provider, model_name=info.model_name, ) self._exporter.increment_counter(EnterpriseTelemetryCounter.TOKENS, info.total_tokens, labels) if info.prompt_tokens > 0: self._exporter.increment_counter(EnterpriseTelemetryCounter.INPUT_TOKENS, info.prompt_tokens, labels) if info.completion_tokens > 0: self._exporter.increment_counter(EnterpriseTelemetryCounter.OUTPUT_TOKENS, info.completion_tokens, labels) status = "failed" if info.error else "success" self._exporter.increment_counter( EnterpriseTelemetryCounter.REQUESTS, 1, self._labels( **labels, type="prompt_generation", status=status, ), ) self._exporter.record_histogram( EnterpriseTelemetryHistogram.PROMPT_GENERATION_DURATION, info.latency, labels, ) if info.error: self._exporter.increment_counter( EnterpriseTelemetryCounter.ERRORS, 1, self._labels( **labels, type="prompt_generation", ), )