diff --git a/api/configs/enterprise/__init__.py b/api/configs/enterprise/__init__.py index 2db9de6195..11a71e1537 100644 --- a/api/configs/enterprise/__init__.py +++ b/api/configs/enterprise/__init__.py @@ -46,8 +46,7 @@ class EnterpriseTelemetryConfig(BaseSettings): ) ENTERPRISE_OTLP_API_KEY: str = Field( - description="Bearer token for enterprise OTLP export authentication. " - "When set, gRPC exporters automatically use TLS (insecure=False).", + description="Bearer token for enterprise OTLP export authentication.", default="", ) diff --git a/api/core/ops/entities/trace_entity.py b/api/core/ops/entities/trace_entity.py index 2eb745aeb9..a6ca1b098b 100644 --- a/api/core/ops/entities/trace_entity.py +++ b/api/core/ops/entities/trace_entity.py @@ -27,6 +27,48 @@ class BaseTraceInfo(BaseModel): model_config = ConfigDict(protected_namespaces=()) + @property + def resolved_trace_id(self) -> str | None: + """Get trace_id with intelligent fallback. + + Priority: + 1. External trace_id (from X-Trace-Id header) + 2. workflow_run_id (if this trace type has it) + 3. message_id (as final fallback) + """ + if self.trace_id: + return self.trace_id + + # Try workflow_run_id (only exists on workflow-related traces) + workflow_run_id = getattr(self, "workflow_run_id", None) + if workflow_run_id: + return workflow_run_id + + # Final fallback to message_id + return str(self.message_id) if self.message_id else None + + @property + def resolved_parent_context(self) -> tuple[str | None, str | None]: + """Resolve cross-workflow parent linking from metadata. + + Extracts typed parent IDs from the untyped ``parent_trace_context`` + metadata dict (set by tool_node when invoking nested workflows). + + Returns: + (trace_correlation_override, parent_span_id_source) where + trace_correlation_override is the outer workflow_run_id and + parent_span_id_source is the outer node_execution_id. + """ + parent_ctx = self.metadata.get("parent_trace_context") + if not isinstance(parent_ctx, dict): + return None, None + trace_override = parent_ctx.get("parent_workflow_run_id") + parent_span = parent_ctx.get("parent_node_execution_id") + return ( + trace_override if isinstance(trace_override, str) else None, + parent_span if isinstance(parent_span, str) else None, + ) + @field_serializer("start_time", "end_time") def serialize_datetime(self, dt: datetime | None) -> str | None: if dt is None: diff --git a/api/core/ops/ops_trace_manager.py b/api/core/ops/ops_trace_manager.py index 3f7bc662fe..a4014111ed 100644 --- a/api/core/ops/ops_trace_manager.py +++ b/api/core/ops/ops_trace_manager.py @@ -945,6 +945,17 @@ class TraceTask: "embedding_model_provider": row[2] or "", } + # Extract rerank model info from retrieval_model kwargs + rerank_model_provider = "" + rerank_model_name = "" + if "retrieval_model" in kwargs: + retrieval_model = kwargs["retrieval_model"] + if isinstance(retrieval_model, dict): + reranking_model = retrieval_model.get("reranking_model") + if isinstance(reranking_model, dict): + rerank_model_provider = reranking_model.get("reranking_provider_name", "") + rerank_model_name = reranking_model.get("reranking_model_name", "") + metadata = { "message_id": message_id, "ls_provider": message_data.model_provider, @@ -961,6 +972,8 @@ class TraceTask: "app_name": app_name, "workspace_name": workspace_name, "embedding_models": embedding_models, + "rerank_model_provider": rerank_model_provider, + "rerank_model_name": rerank_model_name, } if node_execution_id := kwargs.get("node_execution_id"): metadata["node_execution_id"] = node_execution_id @@ -1272,7 +1285,7 @@ class TraceQueueManager: self.trace_instance = OpsTraceManager.get_ops_trace_instance(app_id) self.flask_app = current_app._get_current_object() # type: ignore - from core.telemetry import is_enterprise_telemetry_enabled + from core.telemetry.gateway import is_enterprise_telemetry_enabled self._enterprise_telemetry_enabled = is_enterprise_telemetry_enabled() if trace_manager_timer is None: diff --git a/api/core/telemetry/__init__.py b/api/core/telemetry/__init__.py index b1d25403a0..3cb62bbbbf 100644 --- a/api/core/telemetry/__init__.py +++ b/api/core/telemetry/__init__.py @@ -1,10 +1,7 @@ -"""Community telemetry helpers. +"""Telemetry facade. -Provides ``emit()`` which enqueues trace events into the CE trace pipeline -(``TraceQueueManager`` → ``ops_trace`` Celery queue → Langfuse / LangSmith / etc.). - -Enterprise-only traces (node execution, draft node execution, prompt generation) -are silently dropped when enterprise telemetry is disabled. +Thin public API for emitting telemetry events. All routing logic +lives in ``core.telemetry.gateway`` which is shared by both CE and EE. """ from __future__ import annotations @@ -13,48 +10,34 @@ from typing import TYPE_CHECKING from core.ops.entities.trace_entity import TraceTaskName from core.telemetry.events import TelemetryContext, TelemetryEvent +from core.telemetry.gateway import TRACE_TASK_TO_CASE +from core.telemetry.gateway import emit as gateway_emit if TYPE_CHECKING: from core.ops.ops_trace_manager import TraceQueueManager -_ENTERPRISE_ONLY_TRACES: frozenset[TraceTaskName] = frozenset( - { - TraceTaskName.DRAFT_NODE_EXECUTION_TRACE, - TraceTaskName.NODE_EXECUTION_TRACE, - TraceTaskName.PROMPT_GENERATION_TRACE, - } -) - - -def _is_enterprise_telemetry_enabled() -> bool: - try: - from enterprise.telemetry.exporter import is_enterprise_telemetry_enabled - - return is_enterprise_telemetry_enabled() - except Exception: - return False - def emit(event: TelemetryEvent, trace_manager: TraceQueueManager | None = None) -> None: - from core.ops.ops_trace_manager import TraceQueueManager as LocalTraceQueueManager - from core.ops.ops_trace_manager import TraceTask + """Emit a telemetry event. - if event.name in _ENTERPRISE_ONLY_TRACES and not _is_enterprise_telemetry_enabled(): + Translates the ``TelemetryEvent`` (keyed by ``TraceTaskName``) into a + ``TelemetryCase`` and delegates to ``core.telemetry.gateway.emit()``. + """ + case = TRACE_TASK_TO_CASE.get(event.name) + if case is None: return - queue_manager = trace_manager or LocalTraceQueueManager( - app_id=event.context.app_id, - user_id=event.context.user_id, - ) - queue_manager.add_trace_task(TraceTask(event.name, **event.payload)) + context: dict[str, object] = { + "tenant_id": event.context.tenant_id, + "user_id": event.context.user_id, + "app_id": event.context.app_id, + } + gateway_emit(case, context, event.payload, trace_manager) -is_enterprise_telemetry_enabled = _is_enterprise_telemetry_enabled - __all__ = [ "TelemetryContext", "TelemetryEvent", "TraceTaskName", "emit", - "is_enterprise_telemetry_enabled", ] diff --git a/api/core/telemetry/gateway.py b/api/core/telemetry/gateway.py new file mode 100644 index 0000000000..14c3495ea3 --- /dev/null +++ b/api/core/telemetry/gateway.py @@ -0,0 +1,206 @@ +"""Telemetry gateway — single routing layer for all editions. + +Maps ``TelemetryCase`` → ``CaseRoute`` and dispatches events to either +the CE/EE trace pipeline (``TraceQueueManager``) or the enterprise-only +metric/log Celery queue. + +This module lives in ``core/`` so both CE and EE share one routing table +and one ``emit()`` entry point. No separate enterprise gateway module is +needed — enterprise-specific dispatch (Celery task, payload offloading) +is handled here behind lazy imports that no-op in CE. +""" + +from __future__ import annotations + +import json +import logging +import uuid +from typing import TYPE_CHECKING, Any + +from core.ops.entities.trace_entity import TraceTaskName +from enterprise.telemetry.contracts import CaseRoute, SignalType, TelemetryCase, TelemetryEnvelope +from extensions.ext_storage import storage + +if TYPE_CHECKING: + from core.ops.ops_trace_manager import TraceQueueManager + +logger = logging.getLogger(__name__) + +PAYLOAD_SIZE_THRESHOLD_BYTES = 1 * 1024 * 1024 + +# --------------------------------------------------------------------------- +# Routing table — authoritative mapping for all editions +# --------------------------------------------------------------------------- + +CASE_TO_TRACE_TASK: dict[TelemetryCase, TraceTaskName] = { + TelemetryCase.WORKFLOW_RUN: TraceTaskName.WORKFLOW_TRACE, + TelemetryCase.MESSAGE_RUN: TraceTaskName.MESSAGE_TRACE, + TelemetryCase.NODE_EXECUTION: TraceTaskName.NODE_EXECUTION_TRACE, + TelemetryCase.DRAFT_NODE_EXECUTION: TraceTaskName.DRAFT_NODE_EXECUTION_TRACE, + TelemetryCase.PROMPT_GENERATION: TraceTaskName.PROMPT_GENERATION_TRACE, + TelemetryCase.TOOL_EXECUTION: TraceTaskName.TOOL_TRACE, + TelemetryCase.MODERATION_CHECK: TraceTaskName.MODERATION_TRACE, + TelemetryCase.SUGGESTED_QUESTION: TraceTaskName.SUGGESTED_QUESTION_TRACE, + TelemetryCase.DATASET_RETRIEVAL: TraceTaskName.DATASET_RETRIEVAL_TRACE, + TelemetryCase.GENERATE_NAME: TraceTaskName.GENERATE_NAME_TRACE, +} + +TRACE_TASK_TO_CASE: dict[TraceTaskName, TelemetryCase] = {v: k for k, v in CASE_TO_TRACE_TASK.items()} + +CASE_ROUTING: dict[TelemetryCase, CaseRoute] = { + # TRACE — CE-eligible (flow in both CE and EE) + TelemetryCase.WORKFLOW_RUN: CaseRoute(signal_type=SignalType.TRACE, ce_eligible=True), + TelemetryCase.MESSAGE_RUN: CaseRoute(signal_type=SignalType.TRACE, ce_eligible=True), + TelemetryCase.TOOL_EXECUTION: CaseRoute(signal_type=SignalType.TRACE, ce_eligible=True), + TelemetryCase.MODERATION_CHECK: CaseRoute(signal_type=SignalType.TRACE, ce_eligible=True), + TelemetryCase.SUGGESTED_QUESTION: CaseRoute(signal_type=SignalType.TRACE, ce_eligible=True), + TelemetryCase.DATASET_RETRIEVAL: CaseRoute(signal_type=SignalType.TRACE, ce_eligible=True), + TelemetryCase.GENERATE_NAME: CaseRoute(signal_type=SignalType.TRACE, ce_eligible=True), + # TRACE — enterprise-only + TelemetryCase.NODE_EXECUTION: CaseRoute(signal_type=SignalType.TRACE, ce_eligible=False), + TelemetryCase.DRAFT_NODE_EXECUTION: CaseRoute(signal_type=SignalType.TRACE, ce_eligible=False), + TelemetryCase.PROMPT_GENERATION: CaseRoute(signal_type=SignalType.TRACE, ce_eligible=False), + # METRIC_LOG — enterprise-only (signal-driven, not trace) + TelemetryCase.APP_CREATED: CaseRoute(signal_type=SignalType.METRIC_LOG, ce_eligible=False), + TelemetryCase.APP_UPDATED: CaseRoute(signal_type=SignalType.METRIC_LOG, ce_eligible=False), + TelemetryCase.APP_DELETED: CaseRoute(signal_type=SignalType.METRIC_LOG, ce_eligible=False), + TelemetryCase.FEEDBACK_CREATED: CaseRoute(signal_type=SignalType.METRIC_LOG, ce_eligible=False), +} + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def is_enterprise_telemetry_enabled() -> bool: + try: + from enterprise.telemetry.exporter import is_enterprise_telemetry_enabled + + return is_enterprise_telemetry_enabled() + except Exception: + return False + + +def _handle_payload_sizing( + payload: dict[str, Any], + tenant_id: str, + event_id: str, +) -> tuple[dict[str, Any], str | None]: + """Inline or offload payload based on size. + + Returns ``(payload_for_envelope, storage_key | None)``. Payloads + exceeding ``PAYLOAD_SIZE_THRESHOLD_BYTES`` are written to object + storage and replaced with an empty dict in the envelope. + """ + try: + payload_json = json.dumps(payload) + payload_size = len(payload_json.encode("utf-8")) + except (TypeError, ValueError): + logger.warning("Failed to serialize payload for sizing: event_id=%s", event_id) + return payload, None + + if payload_size <= PAYLOAD_SIZE_THRESHOLD_BYTES: + return payload, None + + storage_key = f"telemetry/{tenant_id}/{event_id}.json" + try: + storage.save(storage_key, payload_json.encode("utf-8")) + logger.debug("Stored large payload to storage: key=%s, size=%d", storage_key, payload_size) + return {}, storage_key + except Exception: + logger.warning("Failed to store large payload, inlining instead: event_id=%s", event_id, exc_info=True) + return payload, None + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + + +def emit( + case: TelemetryCase, + context: dict[str, Any], + payload: dict[str, Any], + trace_manager: TraceQueueManager | None = None, +) -> None: + """Route a telemetry event to the correct pipeline. + + TRACE events are enqueued into ``TraceQueueManager`` (works in both CE + and EE). Enterprise-only traces are silently dropped when EE is + disabled. + + METRIC_LOG events are dispatched to the enterprise Celery queue; + silently dropped when enterprise telemetry is unavailable. + """ + route = CASE_ROUTING.get(case) + if route is None: + logger.warning("Unknown telemetry case: %s, dropping event", case) + return + + if not route.ce_eligible and not is_enterprise_telemetry_enabled(): + logger.debug("Dropping EE-only event: case=%s (EE disabled)", case) + return + + if route.signal_type is SignalType.TRACE: + _emit_trace(case, context, payload, trace_manager) + else: + _emit_metric_log(case, context, payload) + + +def _emit_trace( + case: TelemetryCase, + context: dict[str, Any], + payload: dict[str, Any], + trace_manager: TraceQueueManager | None, +) -> None: + from core.ops.ops_trace_manager import TraceQueueManager as LocalTraceQueueManager + from core.ops.ops_trace_manager import TraceTask + + trace_task_name = CASE_TO_TRACE_TASK.get(case) + if trace_task_name is None: + logger.warning("No TraceTaskName mapping for case: %s", case) + return + + queue_manager = trace_manager or LocalTraceQueueManager( + app_id=context.get("app_id"), + user_id=context.get("user_id"), + ) + queue_manager.add_trace_task(TraceTask(trace_task_name, **payload)) + logger.debug("Enqueued trace task: case=%s, app_id=%s", case, context.get("app_id")) + + +def _emit_metric_log( + case: TelemetryCase, + context: dict[str, Any], + payload: dict[str, Any], +) -> None: + """Build envelope and dispatch to enterprise Celery queue. + + No-ops when the enterprise telemetry task is not importable (CE mode). + """ + try: + from tasks.enterprise_telemetry_task import process_enterprise_telemetry + except ImportError: + logger.debug("Enterprise metric/log dispatch unavailable, dropping: case=%s", case) + return + + tenant_id = context.get("tenant_id", "") + event_id = str(uuid.uuid4()) + + payload_for_envelope, payload_ref = _handle_payload_sizing(payload, tenant_id, event_id) + + envelope = TelemetryEnvelope( + case=case, + tenant_id=tenant_id, + event_id=event_id, + payload=payload_for_envelope, + metadata={"payload_ref": payload_ref} if payload_ref else None, + ) + + process_enterprise_telemetry.delay(envelope.model_dump_json()) + logger.debug( + "Enqueued metric/log event: case=%s, tenant_id=%s, event_id=%s", + case, + tenant_id, + event_id, + ) diff --git a/api/enterprise/telemetry/DATA_DICTIONARY.md b/api/enterprise/telemetry/DATA_DICTIONARY.md index 42797d70c0..c0d07d2550 100644 --- a/api/enterprise/telemetry/DATA_DICTIONARY.md +++ b/api/enterprise/telemetry/DATA_DICTIONARY.md @@ -1,789 +1,405 @@ # Dify Enterprise Telemetry Data Dictionary -This document provides a comprehensive reference for all telemetry signals emitted by the Dify Enterprise OpenTelemetry (OTEL) exporter. It is intended for platform engineers and data scientists integrating Dify with observability stacks like Prometheus, Grafana, Jaeger, or Honeycomb. +Quick reference for all telemetry signals emitted by Dify Enterprise. For configuration and architecture details, see [README.md](./README.md). -## 1. Overview +## Resource Attributes -Dify Enterprise uses a "slim span + rich companion log" architecture to provide high-fidelity observability without overwhelming trace storage. +Attached to every signal (Span, Metric, Log). -- **Traces (Spans)**: Capture the structure, identity, and timing of high-level operations (Workflows and Nodes). -- **Structured Logs**: Provide deep context (inputs, outputs, metadata) for every event, correlated to spans via `trace_id` and `span_id`. -- **Metrics**: Provide 100% accurate counters and histograms for usage, performance, and error tracking. +| Attribute | Type | Example | +|-----------|------|---------| +| `service.name` | string | `dify` | +| `host.name` | string | `dify-api-7f8b` | -### Signal Architecture +## Traces (Spans) -```mermaid -graph TD - A[Workflow Run] -->|Span| B(dify.workflow.run) - A -->|Log| C(dify.workflow.run detail) - B ---|trace_id| C - - D[Node Execution] -->|Span| E(dify.node.execution) - D -->|Log| F(dify.node.execution detail) - E ---|span_id| F - - G[Message/Tool/etc] -->|Log| H(dify.* event) - G -->|Metric| I(dify.* counter/histogram) -``` - -## 2. Configuration - -The Enterprise OTEL exporter is configured via environment variables. - -| Variable | Description | Default | -|----------|-------------|---------| -| `ENTERPRISE_ENABLED` | Master switch for all enterprise features. | `false` | -| `ENTERPRISE_TELEMETRY_ENABLED` | Master switch for enterprise telemetry. | `false` | -| `ENTERPRISE_OTLP_ENDPOINT` | OTLP collector endpoint (e.g., `http://otel-collector:4318`). | - | -| `ENTERPRISE_OTLP_HEADERS` | Custom headers for OTLP requests (e.g., `x-scope-orgid=tenant1`). | - | -| `ENTERPRISE_OTLP_PROTOCOL` | OTLP transport protocol (`http` or `grpc`). | `http` | -| `ENTERPRISE_OTLP_API_KEY` | Bearer token for authentication. | - | -| `ENTERPRISE_INCLUDE_CONTENT` | Whether to include sensitive content (inputs/outputs) in logs. | `true` | -| `ENTERPRISE_SERVICE_NAME` | Service name reported to OTEL. | `dify` | -| `ENTERPRISE_OTEL_SAMPLING_RATE` | Sampling rate for traces (0.0 to 1.0). Metrics are always 100%. | `1.0` | - -## 3. Resource Attributes - -These attributes are attached to every signal (Span, Metric, Log) emitted by the exporter. - -| Attribute | Description | Example | -|-----------|-------------|---------| -| `service.name` | The name of the service. | `dify` | -| `host.name` | The hostname of the container/machine. | `dify-api-7f8b` | - -## 4. Traces (Spans) - -Dify emits spans for long-running or structural operations. - -### 4.1 `dify.workflow.run` - -Represents the full execution of a workflow. +### `dify.workflow.run` | Attribute | Type | Description | |-----------|------|-------------| -| `dify.trace_id` | string | The business trace ID (Workflow Run ID). | -| `dify.tenant_id` | string | Tenant identifier. | -| `dify.app_id` | string | Application identifier. | -| `dify.workflow.id` | string | Workflow definition ID. | -| `dify.workflow.run_id` | string | Unique ID for this specific run. | -| `dify.workflow.status` | string | Final status (`succeeded`, `failed`, `stopped`, etc.). | -| `dify.workflow.error` | string | Error message if the run failed. | -| `dify.workflow.elapsed_time` | float | Total execution time in seconds. | -| `dify.invoke_from` | string | Source of the trigger (`api`, `webapp`, `debug`). | -| `dify.conversation.id` | string | Conversation ID (if applicable). | -| `dify.message.id` | string | Message ID (if applicable). | -| `dify.invoked_by` | string | User ID who triggered the run. | -| `dify.parent.trace_id` | string | (Optional) Trace ID of the parent workflow. | -| `dify.parent.workflow.run_id` | string | (Optional) Run ID of the parent workflow. | -| `dify.parent.node.execution_id` | string | (Optional) Execution ID of the parent node. | -| `dify.parent.app.id` | string | (Optional) App ID of the parent workflow. | +| `dify.trace_id` | string | Business trace ID (Workflow Run ID) | +| `dify.tenant_id` | string | Tenant identifier | +| `dify.app_id` | string | Application identifier | +| `dify.workflow.id` | string | Workflow definition ID | +| `dify.workflow.run_id` | string | Unique ID for this run | +| `dify.workflow.status` | string | `succeeded`, `failed`, `stopped`, etc. | +| `dify.workflow.error` | string | Error message if failed | +| `dify.workflow.elapsed_time` | float | Total execution time (seconds) | +| `dify.invoke_from` | string | `api`, `webapp`, `debug` | +| `dify.conversation.id` | string | Conversation ID (optional) | +| `dify.message.id` | string | Message ID (optional) | +| `dify.invoked_by` | string | User ID who triggered the run | +| `dify.parent.trace_id` | string | Parent workflow trace ID (optional) | +| `dify.parent.workflow.run_id` | string | Parent workflow run ID (optional) | +| `dify.parent.node.execution_id` | string | Parent node execution ID (optional) | +| `dify.parent.app.id` | string | Parent app ID (optional) | -### 4.2 `dify.node.execution` - -Represents the execution of a single node within a workflow. +### `dify.node.execution` | Attribute | Type | Description | |-----------|------|-------------| -| `dify.trace_id` | string | The business trace ID. | -| `dify.tenant_id` | string | Tenant identifier. | -| `dify.app_id` | string | Application identifier. | -| `dify.workflow.id` | string | Workflow definition ID. | -| `dify.workflow.run_id` | string | Workflow Run ID this node belongs to. | -| `dify.message.id` | string | Message ID (if applicable). | -| `dify.conversation.id` | string | Conversation ID (if applicable). | -| `dify.node.execution_id` | string | Unique ID for this node execution. | -| `dify.node.id` | string | Node ID in the workflow graph. | -| `dify.node.type` | string | Type of node (`llm`, `knowledge-retrieval`, `tool`, etc.). | -| `dify.node.title` | string | Display title of the node. | -| `dify.node.status` | string | Execution status (`succeeded`, `failed`). | -| `dify.node.error` | string | Error message if the node failed. | -| `dify.node.elapsed_time` | float | Execution time in seconds. | -| `dify.node.index` | int | Execution order index. | -| `dify.node.predecessor_node_id` | string | ID of the node that triggered this one. | -| `dify.node.iteration_id` | string | (Optional) ID of the iteration this node belongs to. | -| `dify.node.loop_id` | string | (Optional) ID of the loop this node belongs to. | -| `dify.node.parallel_id` | string | (Optional) ID of the parallel branch. | -| `dify.node.invoked_by` | string | User ID who triggered the execution. | +| `dify.trace_id` | string | Business trace ID | +| `dify.tenant_id` | string | Tenant identifier | +| `dify.app_id` | string | Application identifier | +| `dify.workflow.id` | string | Workflow definition ID | +| `dify.workflow.run_id` | string | Workflow Run ID | +| `dify.message.id` | string | Message ID (optional) | +| `dify.conversation.id` | string | Conversation ID (optional) | +| `dify.node.execution_id` | string | Unique node execution ID | +| `dify.node.id` | string | Node ID in workflow graph | +| `dify.node.type` | string | Node type (see appendix) | +| `dify.node.title` | string | Display title | +| `dify.node.status` | string | `succeeded`, `failed` | +| `dify.node.error` | string | Error message if failed | +| `dify.node.elapsed_time` | float | Execution time (seconds) | +| `dify.node.index` | int | Execution order index | +| `dify.node.predecessor_node_id` | string | Triggering node ID | +| `dify.node.iteration_id` | string | Iteration ID (optional) | +| `dify.node.loop_id` | string | Loop ID (optional) | +| `dify.node.parallel_id` | string | Parallel branch ID (optional) | +| `dify.node.invoked_by` | string | User ID who triggered execution | -### 4.3 `dify.node.execution.draft` +### `dify.node.execution.draft` -Identical to `dify.node.execution`, but emitted during "Preview" or "Debug" runs of a single node. +Same attributes as `dify.node.execution`. Emitted during Preview/Debug runs. -## 5. Correlation Model - -Dify uses deterministic ID generation to ensure signals are correlated across different services and asynchronous tasks. - -### ID Generation Rules -- `trace_id`: Derived from the correlation ID (workflow_run_id or node_execution_id for drafts) using `int(UUID(correlation_id))` -- `span_id`: Derived from the source ID using `SHA256(source_id)[:8]` - -### Scenario A: Simple Workflow -A single workflow run with multiple nodes. All spans and logs share the same `trace_id` (derived from `workflow_run_id`). - -``` -trace_id = UUID(workflow_run_id) -├── [root span] dify.workflow.run (span_id = hash(workflow_run_id)) -│ ├── [child] dify.node.execution - "Start" (span_id = hash(node_exec_id_1)) -│ ├── [child] dify.node.execution - "LLM" (span_id = hash(node_exec_id_2)) -│ └── [child] dify.node.execution - "End" (span_id = hash(node_exec_id_3)) -``` - -### Scenario B: Nested Sub-Workflow -A workflow calling another workflow via a Tool or Sub-workflow node. The child workflow's spans are linked to the parent via `parent_span_id`. Both workflows share the same trace_id. - -``` -trace_id = UUID(outer_workflow_run_id) ← shared across both workflows -├── [root] dify.workflow.run (outer) (span_id = hash(outer_workflow_run_id)) -│ ├── dify.node.execution - "Start Node" -│ ├── dify.node.execution - "Tool Node" (triggers sub-workflow) -│ │ └── [child] dify.workflow.run (inner) (span_id = hash(inner_workflow_run_id)) -│ │ ├── dify.node.execution - "Inner Start" -│ │ └── dify.node.execution - "Inner End" -│ └── dify.node.execution - "End Node" -``` - -**Key attributes for nested workflows:** -- Inner workflow's `dify.parent.trace_id` = outer `workflow_run_id` -- Inner workflow's `dify.parent.node.execution_id` = tool node's `execution_id` -- Inner workflow's `dify.parent.workflow.run_id` = outer `workflow_run_id` -- Inner workflow's `dify.parent.app.id` = outer `app_id` - -### Scenario C: Draft Node Execution -A single node run in isolation (debugger/preview mode). It creates its own trace where the node span is the root. - -``` -trace_id = UUID(node_execution_id) ← own trace, NOT part of any workflow -└── dify.node.execution.draft (span_id = hash(node_execution_id)) -``` - -**Key difference:** Draft executions use `node_execution_id` as the correlation_id, so they are NOT children of any workflow trace. - -## 6. Counters +## Counters All counters are cumulative and emitted at 100% accuracy. -### 6.1 Token Counters - -⚠️ **Warning on Double-Counting**: `dify.tokens.total` at the workflow level includes all tokens from its nodes. To get the true total usage for a tenant, you **MUST** filter by `operation_type`. +### Token Counters | Metric | Unit | Description | |--------|------|-------------| -| `dify.tokens.total` | `{token}` | Total tokens consumed. | -| `dify.tokens.input` | `{token}` | Input (prompt) tokens. | -| `dify.tokens.output` | `{token}` | Output (completion) tokens. | +| `dify.tokens.total` | `{token}` | Total tokens consumed | +| `dify.tokens.input` | `{token}` | Input (prompt) tokens | +| `dify.tokens.output` | `{token}` | Output (completion) tokens | -**Labels for Token Counters:** -- `tenant_id`: Tenant identifier. -- `app_id`: Application identifier. -- `operation_type`: `workflow`, `node_execution`, `message`, `rule_generate`, `code_generate`, `structured_output`, `instruction_modify`. -- `model_provider`: LLM provider (e.g., `openai`). -- `model_name`: LLM model (e.g., `gpt-4`). -- `node_type`: Workflow node type (if `operation_type=node_execution`). +**Labels:** +- `tenant_id`, `app_id`, `operation_type`, `model_provider`, `model_name`, `node_type` (if node_execution) -**PromQL Example (Total Input Tokens per Tenant):** -```promql -sum(dify_tokens_input_total{tenant_id="my-tenant", operation_type="workflow"}) -``` +⚠️ **Warning:** `dify.tokens.total` at workflow level includes all node tokens. Filter by `operation_type` to avoid double-counting. -### 6.2 Request Counters +### Request Counters | Metric | Unit | Description | |--------|------|-------------| -| `dify.requests.total` | `{request}` | Total number of operations. | +| `dify.requests.total` | `{request}` | Total operations count | -**Labels vary by `type`:** +**Labels by type:** | `type` | Additional Labels | |--------|-------------------| | `workflow` | `tenant_id`, `app_id`, `status`, `invoke_from` | -| `node` | `tenant_id`, `app_id`, `node_type`, `model_provider`, `status` | -| `draft_node` | `tenant_id`, `app_id`, `node_type`, `model_provider`, `status` | +| `node` | `tenant_id`, `app_id`, `node_type`, `model_provider`, `model_name`, `status` | +| `draft_node` | `tenant_id`, `app_id`, `node_type`, `model_provider`, `model_name`, `status` | | `message` | `tenant_id`, `app_id`, `model_provider`, `model_name`, `status`, `invoke_from` | | `tool` | `tenant_id`, `app_id`, `tool_name` | | `moderation` | `tenant_id`, `app_id` | -| `suggested_question` | `tenant_id`, `app_id` | +| `suggested_question` | `tenant_id`, `app_id`, `model_provider`, `model_name` | | `dataset_retrieval` | `tenant_id`, `app_id` | | `generate_name` | `tenant_id`, `app_id` | | `prompt_generation` | `tenant_id`, `app_id`, `operation_type`, `model_provider`, `model_name`, `status` | -### 6.3 Error Counters +### Error Counters | Metric | Unit | Description | |--------|------|-------------| -| `dify.errors.total` | `{error}` | Total number of failed operations. | +| `dify.errors.total` | `{error}` | Total failed operations | -**Labels vary by `type`:** +**Labels by type:** | `type` | Additional Labels | |--------|-------------------| | `workflow` | `tenant_id`, `app_id` | -| `node` | `tenant_id`, `app_id`, `node_type`, `model_provider` | -| `draft_node` | `tenant_id`, `app_id`, `node_type`, `model_provider` | +| `node` | `tenant_id`, `app_id`, `node_type`, `model_provider`, `model_name` | +| `draft_node` | `tenant_id`, `app_id`, `node_type`, `model_provider`, `model_name` | | `message` | `tenant_id`, `app_id`, `model_provider`, `model_name` | | `tool` | `tenant_id`, `app_id`, `tool_name` | | `prompt_generation` | `tenant_id`, `app_id`, `operation_type`, `model_provider`, `model_name` | -### 6.4 Other Counters +### Other Counters | Metric | Unit | Labels | |--------|------|--------| | `dify.feedback.total` | `{feedback}` | `tenant_id`, `app_id`, `rating` | -| `dify.dataset.retrievals.total` | `{retrieval}` | `tenant_id`, `app_id`, `dataset_id` | +| `dify.dataset.retrievals.total` | `{retrieval}` | `tenant_id`, `app_id`, `dataset_id`, `embedding_model_provider`, `embedding_model`, `rerank_model_provider`, `rerank_model` | | `dify.app.created.total` | `{app}` | `tenant_id`, `app_id`, `mode` | | `dify.app.updated.total` | `{app}` | `tenant_id`, `app_id` | | `dify.app.deleted.total` | `{app}` | `tenant_id`, `app_id` | -## 7. Histograms - -Histograms measure the distribution of durations and latencies. +## Histograms | Metric | Unit | Labels | |--------|------|--------| | `dify.workflow.duration` | `s` | `tenant_id`, `app_id`, `status` | -| `dify.node.duration` | `s` | `tenant_id`, `app_id`, `node_type`, `model_provider`, `plugin_name` (if tool/knowledge) | +| `dify.node.duration` | `s` | `tenant_id`, `app_id`, `node_type`, `model_provider`, `model_name`, `plugin_name` | | `dify.message.duration` | `s` | `tenant_id`, `app_id`, `model_provider`, `model_name` | | `dify.message.time_to_first_token` | `s` | `tenant_id`, `app_id`, `model_provider`, `model_name` | | `dify.tool.duration` | `s` | `tenant_id`, `app_id`, `tool_name` | | `dify.prompt_generation.duration` | `s` | `tenant_id`, `app_id`, `operation_type`, `model_provider`, `model_name` | -**PromQL Example (P95 Node Duration by Type):** -```promql -histogram_quantile(0.95, sum by (le, node_type) (rate(dify_node_duration_bucket[5m]))) -``` +## Structured Logs -## 8. Structured Logs +### Span Companion Logs -Dify emits 13 types of structured logs. Logs are categorized as "Span Companion" (accompanying a span) or "Standalone" (metric-only events). +Logs that accompany spans. Signal type: `span_detail` -### 8.1 Span Companion Logs +#### `dify.workflow.run` Companion Log -These logs contain the full payload for spans, providing rich detail beyond what is captured in the span attributes. - -#### 8.1.1 `dify.workflow.run` Companion Log - -**Signal Type:** `span_detail` - -This log accompanies the `dify.workflow.run` span and includes ALL span attributes PLUS additional detail attributes. - -**Additional Attributes (beyond span attributes):** +**Common attributes:** All span attributes (see Traces section) plus: | Additional Attribute | Type | Always Present | Description | |---------------------|------|----------------|-------------| -| `dify.user.id` | string | No | User identifier (when available) | -| `gen_ai.usage.total_tokens` | int | No | Total tokens consumed by this workflow (sum of all nodes) | -| `dify.workflow.version` | string | Yes | Workflow version identifier | -| `dify.workflow.inputs` | string/JSON | Yes | Workflow input parameters (content-gated) | -| `dify.workflow.outputs` | string/JSON | Yes | Workflow output results (content-gated) | -| `dify.workflow.query` | string | No | User query text (content-gated, for chat workflows) | +| `dify.user.id` | string | No | User identifier | +| `gen_ai.usage.total_tokens` | int | No | Total tokens (sum of all nodes) | +| `dify.workflow.version` | string | Yes | Workflow version | +| `dify.workflow.inputs` | string/JSON | Yes | Input parameters (content-gated) | +| `dify.workflow.outputs` | string/JSON | Yes | Output results (content-gated) | +| `dify.workflow.query` | string | No | User query text (content-gated) | -**Common Log Attributes:** +**Event attributes:** - `dify.event.name`: `"dify.workflow.run"` - `dify.event.signal`: `"span_detail"` -- `trace_id`: Correlated OTEL trace ID (32-char hex) -- `span_id`: Correlated OTEL span ID (16-char hex) -- `tenant_id`: Tenant identifier -- `user_id`: User identifier (when available) +- `trace_id`, `span_id`, `tenant_id`, `user_id` -#### 8.1.2 `dify.node.execution` and `dify.node.execution.draft` Companion Logs +#### `dify.node.execution` and `dify.node.execution.draft` Companion Logs -**Signal Type:** `span_detail` - -These logs accompany node execution spans and include ALL span attributes PLUS additional detail attributes. - -**Additional Attributes (beyond span attributes):** +**Common attributes:** All span attributes (see Traces section) plus: | Additional Attribute | Type | Always Present | Description | |---------------------|------|----------------|-------------| -| `dify.user.id` | string | No | User identifier (when available) | -| `gen_ai.provider.name` | string | No | LLM provider name (for LLM nodes only) | -| `gen_ai.request.model` | string | No | LLM model name (for LLM nodes only) | -| `gen_ai.usage.input_tokens` | int | No | Input tokens for this node (for LLM nodes only) | -| `gen_ai.usage.output_tokens` | int | No | Output tokens for this node (for LLM nodes only) | -| `gen_ai.usage.total_tokens` | int | No | Total tokens for this node (for LLM nodes only) | -| `dify.node.total_price` | float | No | Cost in currency units (for LLM nodes only) | -| `dify.node.currency` | string | No | Currency code (for LLM nodes only) | -| `dify.node.plugin_name` | string | No | Plugin name (for tool/knowledge-retrieval nodes only) | -| `dify.node.plugin_id` | string | No | Plugin identifier (for tool/knowledge-retrieval nodes only) | -| `dify.dataset.id` | string | No | Dataset identifier (for knowledge-retrieval nodes only) | -| `dify.dataset.name` | string | No | Dataset name (for knowledge-retrieval nodes only) | -| `dify.node.inputs` | string/JSON | Yes | Node input data (content-gated) | -| `dify.node.outputs` | string/JSON | Yes | Node output data (content-gated) | -| `dify.node.process_data` | string/JSON | No | Internal processing data (content-gated) | +| `dify.user.id` | string | No | User identifier | +| `gen_ai.provider.name` | string | No | LLM provider (LLM nodes only) | +| `gen_ai.request.model` | string | No | LLM model (LLM nodes only) | +| `gen_ai.usage.input_tokens` | int | No | Input tokens (LLM nodes only) | +| `gen_ai.usage.output_tokens` | int | No | Output tokens (LLM nodes only) | +| `gen_ai.usage.total_tokens` | int | No | Total tokens (LLM nodes only) | +| `dify.node.total_price` | float | No | Cost (LLM nodes only) | +| `dify.node.currency` | string | No | Currency code (LLM nodes only) | +| `dify.node.plugin_name` | string | No | Plugin name (tool/knowledge nodes) | +| `dify.node.plugin_id` | string | No | Plugin ID (tool/knowledge nodes) | +| `dify.dataset.id` | string | No | Dataset ID (knowledge nodes) | +| `dify.dataset.name` | string | No | Dataset name (knowledge nodes) | +| `dify.node.inputs` | string/JSON | Yes | Node inputs (content-gated) | +| `dify.node.outputs` | string/JSON | Yes | Node outputs (content-gated) | +| `dify.node.process_data` | string/JSON | No | Processing data (content-gated) | -**Common Log Attributes:** +**Event attributes:** - `dify.event.name`: `"dify.node.execution"` or `"dify.node.execution.draft"` - `dify.event.signal`: `"span_detail"` -- `trace_id`: Correlated OTEL trace ID (32-char hex) -- `span_id`: Correlated OTEL span ID (16-char hex) -- `tenant_id`: Tenant identifier -- `user_id`: User identifier (when available) +- `trace_id`, `span_id`, `tenant_id`, `user_id` -### 8.2 Standalone Logs +### Standalone Logs -These logs represent events that do not have a structural span. They are emitted as `metric_only` signals. +Logs without structural spans. Signal type: `metric_only` -#### 8.2.1 `dify.message.run` +#### `dify.message.run` -**Signal Type:** `metric_only` +| Attribute | Type | Description | +|-----------|------|-------------| +| `dify.event.name` | string | `"dify.message.run"` | +| `dify.event.signal` | string | `"metric_only"` | +| `trace_id` | string | OTEL trace ID (32-char hex) | +| `span_id` | string | OTEL span ID (16-char hex) | +| `tenant_id` | string | Tenant identifier | +| `user_id` | string | User identifier (optional) | +| `dify.app_id` | string | Application identifier | +| `dify.message.id` | string | Message identifier | +| `dify.conversation.id` | string | Conversation ID (optional) | +| `dify.workflow.run_id` | string | Workflow run ID (optional) | +| `dify.invoke_from` | string | `service-api`, `web-app`, `debugger`, `explore` | +| `gen_ai.provider.name` | string | LLM provider | +| `gen_ai.request.model` | string | LLM model | +| `gen_ai.usage.input_tokens` | int | Input tokens | +| `gen_ai.usage.output_tokens` | int | Output tokens | +| `gen_ai.usage.total_tokens` | int | Total tokens | +| `dify.message.status` | string | `succeeded`, `failed` | +| `dify.message.error` | string | Error message (if failed) | +| `dify.message.duration` | float | Duration (seconds) | +| `dify.message.time_to_first_token` | float | TTFT (seconds) | +| `dify.message.inputs` | string/JSON | Inputs (content-gated) | +| `dify.message.outputs` | string/JSON | Outputs (content-gated) | -Emitted for each message execution (LLM interaction). +#### `dify.tool.execution` -**Attributes:** -- `dify.event.name`: `"dify.message.run"` -- `dify.event.signal`: `"metric_only"` -- `trace_id`: OTEL trace ID (32-char hex) -- `span_id`: OTEL span ID (16-char hex) -- `tenant_id`: Tenant identifier -- `user_id`: User identifier (when available) -- `dify.app_id`: Application identifier -- `dify.message.id`: Message identifier -- `dify.conversation.id`: Conversation identifier (when available) -- `dify.workflow.run_id`: Workflow run identifier (when part of workflow) -- `dify.invoke_from`: Source of invocation (`"service-api"`, `"web-app"`, `"debugger"`, `"explore"`) -- `gen_ai.provider.name`: LLM provider name -- `gen_ai.request.model`: LLM model name -- `gen_ai.usage.input_tokens`: Input tokens consumed -- `gen_ai.usage.output_tokens`: Output tokens generated -- `gen_ai.usage.total_tokens`: Total tokens consumed -- `dify.message.status`: Execution status (`"succeeded"`, `"failed"`) -- `dify.message.error`: Error message (when status is `"failed"`) -- `dify.message.duration`: Execution duration in seconds -- `dify.message.time_to_first_token`: Time to first token in seconds -- `dify.message.inputs`: Message inputs (content-gated) -- `dify.message.outputs`: Message outputs (content-gated) +| Attribute | Type | Description | +|-----------|------|-------------| +| `dify.event.name` | string | `"dify.tool.execution"` | +| `dify.event.signal` | string | `"metric_only"` | +| `trace_id` | string | OTEL trace ID | +| `span_id` | string | OTEL span ID | +| `tenant_id` | string | Tenant identifier | +| `dify.app_id` | string | Application identifier | +| `dify.message.id` | string | Message identifier | +| `dify.tool.name` | string | Tool name | +| `dify.tool.duration` | float | Duration (seconds) | +| `dify.tool.status` | string | `succeeded`, `failed` | +| `dify.tool.error` | string | Error message (if failed) | +| `dify.tool.inputs` | string/JSON | Inputs (content-gated) | +| `dify.tool.outputs` | string/JSON | Outputs (content-gated) | +| `dify.tool.parameters` | string/JSON | Parameters (content-gated) | +| `dify.tool.config` | string/JSON | Configuration (content-gated) | -**Example:** -```json -{ - "trace_id": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", - "span_id": "a1b2c3d4e5f6g7h8", - "attributes": { - "dify.event.name": "dify.message.run", - "dify.event.signal": "metric_only", - "tenant_id": "550e8400-e29b-41d4-a716-446655440000", - "user_id": "660e8400-e29b-41d4-a716-446655440001", - "dify.app_id": "770e8400-e29b-41d4-a716-446655440002", - "dify.message.id": "880e8400-e29b-41d4-a716-446655440003", - "dify.conversation.id": "990e8400-e29b-41d4-a716-446655440004", - "dify.invoke_from": "web-app", - "gen_ai.provider.name": "openai", - "gen_ai.request.model": "gpt-4", - "gen_ai.usage.input_tokens": 120, - "gen_ai.usage.output_tokens": 85, - "gen_ai.usage.total_tokens": 205, - "dify.message.status": "succeeded", - "dify.message.duration": 2.45, - "dify.message.time_to_first_token": 0.32, - "dify.message.inputs": "{\"query\": \"What is the weather?\"}", - "dify.message.outputs": "{\"answer\": \"The weather is sunny.\"}" - } -} -``` +#### `dify.moderation.check` -#### 8.2.2 `dify.tool.execution` +| Attribute | Type | Description | +|-----------|------|-------------| +| `dify.event.name` | string | `"dify.moderation.check"` | +| `dify.event.signal` | string | `"metric_only"` | +| `trace_id` | string | OTEL trace ID | +| `span_id` | string | OTEL span ID | +| `tenant_id` | string | Tenant identifier | +| `dify.app_id` | string | Application identifier | +| `dify.message.id` | string | Message identifier | +| `dify.moderation.type` | string | `input`, `output` | +| `dify.moderation.action` | string | `pass`, `block`, `flag` | +| `dify.moderation.flagged` | boolean | Whether flagged | +| `dify.moderation.categories` | JSON array | Flagged categories | +| `dify.moderation.query` | string | Content (content-gated) | -**Signal Type:** `metric_only` +#### `dify.suggested_question.generation` -Emitted for each tool invocation. +| Attribute | Type | Description | +|-----------|------|-------------| +| `dify.event.name` | string | `"dify.suggested_question.generation"` | +| `dify.event.signal` | string | `"metric_only"` | +| `trace_id` | string | OTEL trace ID | +| `span_id` | string | OTEL span ID | +| `tenant_id` | string | Tenant identifier | +| `dify.app_id` | string | Application identifier | +| `dify.message.id` | string | Message identifier | +| `dify.suggested_question.count` | int | Number of questions | +| `dify.suggested_question.duration` | float | Duration (seconds) | +| `dify.suggested_question.status` | string | `succeeded`, `failed` | +| `dify.suggested_question.error` | string | Error message (if failed) | +| `dify.suggested_question.questions` | JSON array | Questions (content-gated) | -**Attributes:** -- `dify.event.name`: `"dify.tool.execution"` -- `dify.event.signal`: `"metric_only"` -- `trace_id`: OTEL trace ID -- `span_id`: OTEL span ID -- `tenant_id`: Tenant identifier -- `dify.app_id`: Application identifier -- `dify.message.id`: Message identifier -- `dify.tool.name`: Tool name -- `dify.tool.duration`: Execution duration in seconds -- `dify.tool.status`: Execution status (`"succeeded"`, `"failed"`) -- `dify.tool.error`: Error message (when failed) -- `dify.tool.inputs`: Tool inputs (content-gated) -- `dify.tool.outputs`: Tool outputs (content-gated) -- `dify.tool.parameters`: Tool parameters (content-gated) -- `dify.tool.config`: Tool configuration (content-gated) +#### `dify.dataset.retrieval` -**Example:** -```json -{ - "trace_id": "b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7", - "span_id": "b2c3d4e5f6g7h8i9", - "attributes": { - "dify.event.name": "dify.tool.execution", - "dify.event.signal": "metric_only", - "tenant_id": "550e8400-e29b-41d4-a716-446655440000", - "dify.app_id": "770e8400-e29b-41d4-a716-446655440002", - "dify.message.id": "880e8400-e29b-41d4-a716-446655440003", - "dify.tool.name": "weather_api", - "dify.tool.duration": 0.85, - "dify.tool.status": "succeeded", - "dify.tool.inputs": "{\"location\": \"San Francisco\"}", - "dify.tool.outputs": "{\"temperature\": 72, \"condition\": \"sunny\"}", - "dify.tool.parameters": "{\"api_key\": \"***\"}", - "dify.tool.config": "{\"timeout\": 30}" - } -} -``` +| Attribute | Type | Description | +|-----------|------|-------------| +| `dify.event.name` | string | `"dify.dataset.retrieval"` | +| `dify.event.signal` | string | `"metric_only"` | +| `trace_id` | string | OTEL trace ID | +| `span_id` | string | OTEL span ID | +| `tenant_id` | string | Tenant identifier | +| `dify.app_id` | string | Application identifier | +| `dify.message.id` | string | Message identifier | +| `dify.dataset.id` | string | Dataset identifier | +| `dify.dataset.name` | string | Dataset name | +| `dify.dataset.embedding_providers` | JSON array | Embedding model providers (one per dataset) | +| `dify.dataset.embedding_models` | JSON array | Embedding models (one per dataset) | +| `dify.retrieval.rerank_provider` | string | Rerank model provider | +| `dify.retrieval.rerank_model` | string | Rerank model name | +| `dify.retrieval.query` | string | Search query (content-gated) | +| `dify.retrieval.document_count` | int | Documents retrieved | +| `dify.retrieval.duration` | float | Duration (seconds) | +| `dify.retrieval.status` | string | `succeeded`, `failed` | +| `dify.retrieval.error` | string | Error message (if failed) | +| `dify.dataset.documents` | JSON array | Documents (content-gated) | -#### 8.2.3 `dify.moderation.check` +#### `dify.generate_name.execution` -**Signal Type:** `metric_only` +| Attribute | Type | Description | +|-----------|------|-------------| +| `dify.event.name` | string | `"dify.generate_name.execution"` | +| `dify.event.signal` | string | `"metric_only"` | +| `trace_id` | string | OTEL trace ID | +| `span_id` | string | OTEL span ID | +| `tenant_id` | string | Tenant identifier | +| `dify.app_id` | string | Application identifier | +| `dify.conversation.id` | string | Conversation identifier | +| `dify.generate_name.duration` | float | Duration (seconds) | +| `dify.generate_name.status` | string | `succeeded`, `failed` | +| `dify.generate_name.error` | string | Error message (if failed) | +| `dify.generate_name.inputs` | string/JSON | Inputs (content-gated) | +| `dify.generate_name.outputs` | string | Generated name (content-gated) | -Emitted for content moderation checks. +#### `dify.prompt_generation.execution` -**Attributes:** -- `dify.event.name`: `"dify.moderation.check"` -- `dify.event.signal`: `"metric_only"` -- `trace_id`: OTEL trace ID -- `span_id`: OTEL span ID -- `tenant_id`: Tenant identifier -- `dify.app_id`: Application identifier -- `dify.message.id`: Message identifier -- `dify.moderation.type`: Moderation type (`"input"`, `"output"`) -- `dify.moderation.action`: Action taken (`"pass"`, `"block"`, `"flag"`) -- `dify.moderation.flagged`: Whether content was flagged (boolean) -- `dify.moderation.categories`: Flagged categories (JSON array) -- `dify.moderation.query`: Content being moderated (content-gated) +| Attribute | Type | Description | +|-----------|------|-------------| +| `dify.event.name` | string | `"dify.prompt_generation.execution"` | +| `dify.event.signal` | string | `"metric_only"` | +| `trace_id` | string | OTEL trace ID | +| `span_id` | string | OTEL span ID | +| `tenant_id` | string | Tenant identifier | +| `dify.app_id` | string | Application identifier | +| `dify.prompt_generation.operation_type` | string | Operation type (see appendix) | +| `gen_ai.provider.name` | string | LLM provider | +| `gen_ai.request.model` | string | LLM model | +| `gen_ai.usage.input_tokens` | int | Input tokens | +| `gen_ai.usage.output_tokens` | int | Output tokens | +| `gen_ai.usage.total_tokens` | int | Total tokens | +| `dify.prompt_generation.duration` | float | Duration (seconds) | +| `dify.prompt_generation.status` | string | `succeeded`, `failed` | +| `dify.prompt_generation.error` | string | Error message (if failed) | +| `dify.prompt_generation.instruction` | string | Instruction (content-gated) | +| `dify.prompt_generation.output` | string/JSON | Output (content-gated) | -**Example:** -```json -{ - "trace_id": "c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8", - "span_id": "c3d4e5f6g7h8i9j0", - "attributes": { - "dify.event.name": "dify.moderation.check", - "dify.event.signal": "metric_only", - "tenant_id": "550e8400-e29b-41d4-a716-446655440000", - "dify.app_id": "770e8400-e29b-41d4-a716-446655440002", - "dify.message.id": "880e8400-e29b-41d4-a716-446655440003", - "dify.moderation.type": "input", - "dify.moderation.action": "pass", - "dify.moderation.flagged": false, - "dify.moderation.categories": "[]", - "dify.moderation.query": "What is the weather?" - } -} -``` +#### `dify.app.created` -#### 8.2.4 `dify.suggested_question.generation` +| Attribute | Type | Description | +|-----------|------|-------------| +| `dify.event.name` | string | `"dify.app.created"` | +| `dify.event.signal` | string | `"metric_only"` | +| `tenant_id` | string | Tenant identifier | +| `dify.app_id` | string | Application identifier | +| `dify.app.mode` | string | `chat`, `completion`, `agent-chat`, `workflow` | +| `dify.app.created_at` | string | Timestamp (ISO 8601) | -**Signal Type:** `metric_only` +#### `dify.app.updated` -Emitted when suggested questions are generated. +| Attribute | Type | Description | +|-----------|------|-------------| +| `dify.event.name` | string | `"dify.app.updated"` | +| `dify.event.signal` | string | `"metric_only"` | +| `tenant_id` | string | Tenant identifier | +| `dify.app_id` | string | Application identifier | +| `dify.app.updated_at` | string | Timestamp (ISO 8601) | -**Attributes:** -- `dify.event.name`: `"dify.suggested_question.generation"` -- `dify.event.signal`: `"metric_only"` -- `trace_id`: OTEL trace ID -- `span_id`: OTEL span ID -- `tenant_id`: Tenant identifier -- `dify.app_id`: Application identifier -- `dify.message.id`: Message identifier -- `dify.suggested_question.count`: Number of questions generated -- `dify.suggested_question.duration`: Generation duration in seconds -- `dify.suggested_question.status`: Generation status (`"succeeded"`, `"failed"`) -- `dify.suggested_question.error`: Error message (when failed) -- `dify.suggested_question.questions`: Generated questions (content-gated, JSON array) +#### `dify.app.deleted` -**Example:** -```json -{ - "trace_id": "d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9", - "span_id": "d4e5f6g7h8i9j0k1", - "attributes": { - "dify.event.name": "dify.suggested_question.generation", - "dify.event.signal": "metric_only", - "tenant_id": "550e8400-e29b-41d4-a716-446655440000", - "dify.app_id": "770e8400-e29b-41d4-a716-446655440002", - "dify.message.id": "880e8400-e29b-41d4-a716-446655440003", - "dify.suggested_question.count": 3, - "dify.suggested_question.duration": 1.2, - "dify.suggested_question.status": "succeeded", - "dify.suggested_question.questions": "[\"What about tomorrow?\", \"How about next week?\", \"Is it raining?\"]" - } -} -``` +| Attribute | Type | Description | +|-----------|------|-------------| +| `dify.event.name` | string | `"dify.app.deleted"` | +| `dify.event.signal` | string | `"metric_only"` | +| `tenant_id` | string | Tenant identifier | +| `dify.app_id` | string | Application identifier | +| `dify.app.deleted_at` | string | Timestamp (ISO 8601) | -#### 8.2.5 `dify.dataset.retrieval` +#### `dify.feedback.created` -**Signal Type:** `metric_only` +| Attribute | Type | Description | +|-----------|------|-------------| +| `dify.event.name` | string | `"dify.feedback.created"` | +| `dify.event.signal` | string | `"metric_only"` | +| `trace_id` | string | OTEL trace ID | +| `span_id` | string | OTEL span ID | +| `tenant_id` | string | Tenant identifier | +| `dify.app_id` | string | Application identifier | +| `dify.message.id` | string | Message identifier | +| `dify.feedback.rating` | string | `like`, `dislike`, `null` | +| `dify.feedback.content` | string | Feedback text (content-gated) | +| `dify.feedback.created_at` | string | Timestamp (ISO 8601) | -Emitted for knowledge base retrieval operations. +#### `dify.telemetry.rehydration_failed` -**Attributes:** -- `dify.event.name`: `"dify.dataset.retrieval"` -- `dify.event.signal`: `"metric_only"` -- `trace_id`: OTEL trace ID -- `span_id`: OTEL span ID -- `tenant_id`: Tenant identifier -- `dify.app_id`: Application identifier -- `dify.message.id`: Message identifier -- `dify.dataset.id`: Dataset identifier -- `dify.dataset.name`: Dataset name -- `dify.retrieval.query`: Search query (content-gated) -- `dify.retrieval.document_count`: Number of documents retrieved -- `dify.retrieval.duration`: Retrieval duration in seconds -- `dify.retrieval.status`: Retrieval status (`"succeeded"`, `"failed"`) -- `dify.retrieval.error`: Error message (when failed) -- `dify.dataset.documents`: Retrieved documents (content-gated, JSON array) +Diagnostic event for telemetry system health monitoring. -**Example:** -```json -{ - "trace_id": "e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0", - "span_id": "e5f6g7h8i9j0k1l2", - "attributes": { - "dify.event.name": "dify.dataset.retrieval", - "dify.event.signal": "metric_only", - "tenant_id": "550e8400-e29b-41d4-a716-446655440000", - "dify.app_id": "770e8400-e29b-41d4-a716-446655440002", - "dify.message.id": "880e8400-e29b-41d4-a716-446655440003", - "dify.dataset.id": "aa0e8400-e29b-41d4-a716-446655440005", - "dify.dataset.name": "Product Documentation", - "dify.retrieval.query": "installation guide", - "dify.retrieval.document_count": 5, - "dify.retrieval.duration": 0.45, - "dify.retrieval.status": "succeeded", - "dify.dataset.documents": "[{\"id\": \"doc1\", \"score\": 0.95}, {\"id\": \"doc2\", \"score\": 0.87}]" - } -} -``` +| Attribute | Type | Description | +|-----------|------|-------------| +| `dify.event.name` | string | `"dify.telemetry.rehydration_failed"` | +| `dify.event.signal` | string | `"metric_only"` | +| `tenant_id` | string | Tenant identifier | +| `dify.telemetry.error` | string | Error message | +| `dify.telemetry.payload_type` | string | Payload type (see appendix) | +| `dify.telemetry.correlation_id` | string | Correlation ID | -#### 8.2.6 `dify.generate_name.execution` +## Content-Gated Attributes -**Signal Type:** `metric_only` - -Emitted when conversation names are auto-generated. - -**Attributes:** -- `dify.event.name`: `"dify.generate_name.execution"` -- `dify.event.signal`: `"metric_only"` -- `trace_id`: OTEL trace ID -- `span_id`: OTEL span ID -- `tenant_id`: Tenant identifier -- `dify.app_id`: Application identifier -- `dify.conversation.id`: Conversation identifier -- `dify.generate_name.duration`: Generation duration in seconds -- `dify.generate_name.status`: Generation status (`"succeeded"`, `"failed"`) -- `dify.generate_name.error`: Error message (when failed) -- `dify.generate_name.inputs`: Generation inputs (content-gated) -- `dify.generate_name.outputs`: Generated name (content-gated) - -**Example:** -```json -{ - "trace_id": "f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1", - "span_id": "f6g7h8i9j0k1l2m3", - "attributes": { - "dify.event.name": "dify.generate_name.execution", - "dify.event.signal": "metric_only", - "tenant_id": "550e8400-e29b-41d4-a716-446655440000", - "dify.app_id": "770e8400-e29b-41d4-a716-446655440002", - "dify.conversation.id": "990e8400-e29b-41d4-a716-446655440004", - "dify.generate_name.duration": 0.75, - "dify.generate_name.status": "succeeded", - "dify.generate_name.inputs": "{\"first_message\": \"What is the weather?\"}", - "dify.generate_name.outputs": "Weather Inquiry" - } -} -``` - -#### 8.2.7 `dify.prompt_generation.execution` - -**Signal Type:** `metric_only` - -Emitted for prompt engineering operations. - -**Attributes:** -- `dify.event.name`: `"dify.prompt_generation.execution"` -- `dify.event.signal`: `"metric_only"` -- `trace_id`: OTEL trace ID -- `span_id`: OTEL span ID -- `tenant_id`: Tenant identifier -- `dify.app_id`: Application identifier -- `dify.prompt_generation.operation_type`: Operation type (`"rule_generate"`, `"code_generate"`, `"structured_output"`, `"instruction_modify"`) -- `gen_ai.provider.name`: LLM provider name -- `gen_ai.request.model`: LLM model name -- `gen_ai.usage.input_tokens`: Input tokens consumed -- `gen_ai.usage.output_tokens`: Output tokens generated -- `gen_ai.usage.total_tokens`: Total tokens consumed -- `dify.prompt_generation.duration`: Generation duration in seconds -- `dify.prompt_generation.status`: Generation status (`"succeeded"`, `"failed"`) -- `dify.prompt_generation.error`: Error message (when failed) -- `dify.prompt_generation.instruction`: Prompt instruction (content-gated) -- `dify.prompt_generation.output`: Generated output (content-gated) - -**Example:** -```json -{ - "trace_id": "g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2", - "span_id": "g7h8i9j0k1l2m3n4", - "attributes": { - "dify.event.name": "dify.prompt_generation.execution", - "dify.event.signal": "metric_only", - "tenant_id": "550e8400-e29b-41d4-a716-446655440000", - "dify.app_id": "770e8400-e29b-41d4-a716-446655440002", - "dify.prompt_generation.operation_type": "rule_generate", - "gen_ai.provider.name": "openai", - "gen_ai.request.model": "gpt-4", - "gen_ai.usage.input_tokens": 50, - "gen_ai.usage.output_tokens": 30, - "gen_ai.usage.total_tokens": 80, - "dify.prompt_generation.duration": 1.1, - "dify.prompt_generation.status": "succeeded", - "dify.prompt_generation.instruction": "Generate validation rules", - "dify.prompt_generation.output": "{\"rules\": [\"check_length\", \"validate_format\"]}" - } -} -``` - -#### 8.2.8 `dify.app.created` - -**Signal Type:** `metric_only` - -Emitted when an application is created. - -**Attributes:** -- `dify.event.name`: `"dify.app.created"` -- `dify.event.signal`: `"metric_only"` -- `tenant_id`: Tenant identifier -- `dify.app_id`: Application identifier -- `dify.app.mode`: Application mode (`"chat"`, `"completion"`, `"agent-chat"`, `"workflow"`) -- `dify.app.created_at`: Creation timestamp (ISO 8601) - -**Example:** -```json -{ - "attributes": { - "dify.event.name": "dify.app.created", - "dify.event.signal": "metric_only", - "tenant_id": "550e8400-e29b-41d4-a716-446655440000", - "dify.app_id": "770e8400-e29b-41d4-a716-446655440002", - "dify.app.mode": "workflow", - "dify.app.created_at": "2026-02-10T19:30:00Z" - } -} -``` - -#### 8.2.9 `dify.app.updated` - -**Signal Type:** `metric_only` - -Emitted when an application is updated. - -**Attributes:** -- `dify.event.name`: `"dify.app.updated"` -- `dify.event.signal`: `"metric_only"` -- `tenant_id`: Tenant identifier -- `dify.app_id`: Application identifier -- `dify.app.updated_at`: Update timestamp (ISO 8601) - -**Example:** -```json -{ - "attributes": { - "dify.event.name": "dify.app.updated", - "dify.event.signal": "metric_only", - "tenant_id": "550e8400-e29b-41d4-a716-446655440000", - "dify.app_id": "770e8400-e29b-41d4-a716-446655440002", - "dify.app.updated_at": "2026-02-10T20:15:00Z" - } -} -``` - -#### 8.2.10 `dify.app.deleted` - -**Signal Type:** `metric_only` - -Emitted when an application is deleted. - -**Attributes:** -- `dify.event.name`: `"dify.app.deleted"` -- `dify.event.signal`: `"metric_only"` -- `tenant_id`: Tenant identifier -- `dify.app_id`: Application identifier -- `dify.app.deleted_at`: Deletion timestamp (ISO 8601) - -**Example:** -```json -{ - "attributes": { - "dify.event.name": "dify.app.deleted", - "dify.event.signal": "metric_only", - "tenant_id": "550e8400-e29b-41d4-a716-446655440000", - "dify.app_id": "770e8400-e29b-41d4-a716-446655440002", - "dify.app.deleted_at": "2026-02-10T21:00:00Z" - } -} -``` - -#### 8.2.11 `dify.feedback.created` - -**Signal Type:** `metric_only` - -Emitted when user feedback is submitted. - -**Attributes:** -- `dify.event.name`: `"dify.feedback.created"` -- `dify.event.signal`: `"metric_only"` -- `trace_id`: OTEL trace ID -- `span_id`: OTEL span ID -- `tenant_id`: Tenant identifier -- `dify.app_id`: Application identifier -- `dify.message.id`: Message identifier -- `dify.feedback.rating`: Rating (`"like"`, `"dislike"`, `null`) -- `dify.feedback.content`: Feedback text (content-gated, omitted when gating enabled) -- `dify.feedback.created_at`: Feedback timestamp (ISO 8601) - -**Example:** -```json -{ - "trace_id": "h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3", - "span_id": "h8i9j0k1l2m3n4o5", - "attributes": { - "dify.event.name": "dify.feedback.created", - "dify.event.signal": "metric_only", - "tenant_id": "550e8400-e29b-41d4-a716-446655440000", - "dify.app_id": "770e8400-e29b-41d4-a716-446655440002", - "dify.message.id": "880e8400-e29b-41d4-a716-446655440003", - "dify.feedback.rating": "like", - "dify.feedback.content": "Very helpful response!", - "dify.feedback.created_at": "2026-02-10T19:45:00Z" - } -} -``` - -#### 8.2.12 `dify.telemetry.rehydration_failed` - -**Signal Type:** `metric_only` (Diagnostic) - -Emitted when telemetry payload rehydration fails. This is a diagnostic event for monitoring telemetry system health. - -**Attributes:** -- `dify.event.name`: `"dify.telemetry.rehydration_failed"` -- `dify.event.signal`: `"metric_only"` -- `tenant_id`: Tenant identifier -- `dify.telemetry.error`: Error message -- `dify.telemetry.payload_type`: Type of payload that failed (`"workflow"`, `"node"`, `"message"`, etc.) -- `dify.telemetry.correlation_id`: Correlation ID of the failed payload - -**Example:** -```json -{ - "attributes": { - "dify.event.name": "dify.telemetry.rehydration_failed", - "dify.event.signal": "metric_only", - "tenant_id": "550e8400-e29b-41d4-a716-446655440000", - "dify.telemetry.error": "Workflow run not found in database", - "dify.telemetry.payload_type": "workflow", - "dify.telemetry.correlation_id": "bb0e8400-e29b-41d4-a716-446655440006" - } -} -``` - -## 9. Content Attributes - -When `ENTERPRISE_INCLUDE_CONTENT` is set to `false`, the following attributes are replaced with a reference string (e.g., `ref:workflow_run_id=...`) to prevent sensitive data leakage to the OTEL collector. +When `ENTERPRISE_INCLUDE_CONTENT=false`, these attributes are replaced with reference strings (`ref:{id_type}={uuid}`). | Attribute | Signal | |-----------|--------| @@ -809,103 +425,28 @@ When `ENTERPRISE_INCLUDE_CONTENT` is set to `false`, the following attributes ar | `dify.prompt_generation.output` | `dify.prompt_generation.execution` | | `dify.feedback.content` | `dify.feedback.created` | -### Content Gating Behavior +## Appendix -When `ENTERPRISE_INCLUDE_CONTENT=true` (default), content attributes contain the actual data: +### Operation Types -```json -{ - "dify.workflow.inputs": "{\"query\": \"What is the weather?\", \"location\": \"San Francisco\"}", - "dify.workflow.outputs": "{\"answer\": \"The weather in San Francisco is sunny, 72°F.\"}", - "dify.workflow.query": "What is the weather?" -} -``` +- `workflow`, `node_execution`, `message`, `rule_generate`, `code_generate`, `structured_output`, `instruction_modify` -When `ENTERPRISE_INCLUDE_CONTENT=false`, content attributes are replaced with reference strings: +### Node Types -```json -{ - "dify.workflow.inputs": "ref:workflow_run_id=550e8400-e29b-41d4-a716-446655440000", - "dify.workflow.outputs": "ref:workflow_run_id=550e8400-e29b-41d4-a716-446655440000", - "dify.workflow.query": "ref:workflow_run_id=550e8400-e29b-41d4-a716-446655440000" -} -``` - -### Reference String Format - -Reference strings follow the pattern `ref:{id_type}={uuid}`, where: - -- `{id_type}` identifies the entity type -- `{uuid}` is the entity's unique identifier - -**Examples:** - -``` -ref:workflow_run_id=550e8400-e29b-41d4-a716-446655440000 -ref:node_execution_id=660e8400-e29b-41d4-a716-446655440001 -ref:message_id=770e8400-e29b-41d4-a716-446655440002 -ref:conversation_id=880e8400-e29b-41d4-a716-446655440003 -ref:trace_id=990e8400-e29b-41d4-a716-446655440004 -``` - -**Usage:** To retrieve the actual content, query your Dify database using the provided UUID. For example, `ref:workflow_run_id=550e8400-e29b-41d4-a716-446655440000` means you can fetch the workflow run record from the `workflow_runs` table using that ID. - -**Special Case:** The `dify.feedback.content` attribute is **omitted entirely** (not replaced with a reference string) when content gating is enabled, as feedback text is not stored with a retrievable ID. - -## 10. Appendix - -### Operation Types (`operation_type`) -- `workflow` -- `node_execution` -- `message` -- `rule_generate` -- `code_generate` -- `structured_output` -- `instruction_modify` - -### Node Types (`node_type`) -- `start`, `end`, `answer`, `llm`, `knowledge-retrieval`, `knowledge-index`, `if-else`, `code`, `template-transform`, `question-classifier`, `http-request`, `tool`, `datasource`, `variable-aggregator`, `loop`, `iteration`, `parameter-extractor`, `assigner`, `document-extractor`, `list-operator`, `agent`, `trigger-webhook`, `trigger-schedule`, `trigger-plugin`, `human-input`. +- `start`, `end`, `answer`, `llm`, `knowledge-retrieval`, `knowledge-index`, `if-else`, `code`, `template-transform`, `question-classifier`, `http-request`, `tool`, `datasource`, `variable-aggregator`, `loop`, `iteration`, `parameter-extractor`, `assigner`, `document-extractor`, `list-operator`, `agent`, `trigger-webhook`, `trigger-schedule`, `trigger-plugin`, `human-input` ### Workflow Statuses -- `running`, `succeeded`, `failed`, `stopped`, `partial-succeeded`, `paused`. + +- `running`, `succeeded`, `failed`, `stopped`, `partial-succeeded`, `paused` + +### Payload Types + +- `workflow`, `node`, `message`, `tool`, `moderation`, `suggested_question`, `dataset_retrieval`, `generate_name`, `prompt_generation`, `app`, `feedback` ### Null Value Behavior -The handling of null/missing values differs between spans (traces) and structured logs: +**Spans:** Attributes with `null` values are omitted. -**In Spans (Traces):** -Attributes with `None`/`null` values are **omitted** from the exported span data. This follows OpenTelemetry conventions for efficient trace storage. +**Logs:** Attributes with `null` values appear as `null` in JSON. -Example - a workflow span without optional parent attributes: -```python -# These attributes are NOT included in the span: -# - dify.parent.trace_id (null) -# - dify.parent.workflow.run_id (null) -# - dify.parent.node.execution_id (null) -# - dify.parent.app.id (null) -``` - -**In Structured Logs:** -Attributes with `null` values appear as `null` in the JSON payload. - -Example: -```json -{ - "attributes": { - "dify.workflow.error": null, - "dify.conversation.id": null - } -} -``` - -**Conditional Attributes:** -Many attributes are only present under specific conditions: - -- **LLM-related attributes** (`gen_ai.provider.name`, `gen_ai.usage.total_tokens`, etc.) only appear for LLM nodes -- **Plugin attributes** (`dify.node.plugin_name`, `dify.node.plugin_id`) only appear for tool/knowledge-retrieval nodes -- **Dataset attributes** (`dify.dataset.id`, `dify.dataset.name`) only appear for knowledge-retrieval nodes -- **Error attributes** (`dify.workflow.error`, `dify.node.error`) only appear when status is `"failed"` -- **Parent attributes** (`dify.parent.*`) only appear for nested/sub-workflow executions - -**Content-Gated Attributes:** -When `ENTERPRISE_INCLUDE_CONTENT=false`, content attributes are replaced with reference strings (not set to `null`). See Section 9 for details. +**Content-Gated:** Replaced with reference strings, not set to `null`. diff --git a/api/enterprise/telemetry/README.md b/api/enterprise/telemetry/README.md new file mode 100644 index 0000000000..2c8ca988e1 --- /dev/null +++ b/api/enterprise/telemetry/README.md @@ -0,0 +1,116 @@ +# Dify Enterprise Telemetry + +This document provides an overview of the Dify Enterprise OpenTelemetry (OTEL) exporter and how to configure it for integration with observability stacks like Prometheus, Grafana, Jaeger, or Honeycomb. + +## Overview + +Dify Enterprise uses a "slim span + rich companion log" architecture to provide high-fidelity observability without overwhelming trace storage. + +- **Traces (Spans)**: Capture the structure, identity, and timing of high-level operations (Workflows and Nodes). +- **Structured Logs**: Provide deep context (inputs, outputs, metadata) for every event, correlated to spans via `trace_id` and `span_id`. +- **Metrics**: Provide 100% accurate counters and histograms for usage, performance, and error tracking. + +### Signal Architecture + +```mermaid +graph TD + A[Workflow Run] -->|Span| B(dify.workflow.run) + A -->|Log| C(dify.workflow.run detail) + B ---|trace_id| C + + D[Node Execution] -->|Span| E(dify.node.execution) + D -->|Log| F(dify.node.execution detail) + E ---|span_id| F + + G[Message/Tool/etc] -->|Log| H(dify.* event) + G -->|Metric| I(dify.* counter/histogram) +``` + +## Configuration + +The Enterprise OTEL exporter is configured via environment variables. + +| Variable | Description | Default | +|----------|-------------|---------| +| `ENTERPRISE_ENABLED` | Master switch for all enterprise features. | `false` | +| `ENTERPRISE_TELEMETRY_ENABLED` | Master switch for enterprise telemetry. | `false` | +| `ENTERPRISE_OTLP_ENDPOINT` | OTLP collector endpoint (e.g., `http://otel-collector:4318`). | - | +| `ENTERPRISE_OTLP_HEADERS` | Custom headers for OTLP requests (e.g., `x-scope-orgid=tenant1`). | - | +| `ENTERPRISE_OTLP_PROTOCOL` | OTLP transport protocol (`http` or `grpc`). | `http` | +| `ENTERPRISE_OTLP_API_KEY` | Bearer token for authentication. | - | +| `ENTERPRISE_INCLUDE_CONTENT` | Whether to include sensitive content (inputs/outputs) in logs. | `true` | +| `ENTERPRISE_SERVICE_NAME` | Service name reported to OTEL. | `dify` | +| `ENTERPRISE_OTEL_SAMPLING_RATE` | Sampling rate for traces (0.0 to 1.0). Metrics are always 100%. | `1.0` | + +## Correlation Model + +Dify uses deterministic ID generation to ensure signals are correlated across different services and asynchronous tasks. + +### ID Generation Rules +- `trace_id`: Derived from the correlation ID (workflow_run_id or node_execution_id for drafts) using `int(UUID(correlation_id))` +- `span_id`: Derived from the source ID using `SHA256(source_id)[:8]` + +### Scenario A: Simple Workflow +A single workflow run with multiple nodes. All spans and logs share the same `trace_id` (derived from `workflow_run_id`). + +``` +trace_id = UUID(workflow_run_id) +├── [root span] dify.workflow.run (span_id = hash(workflow_run_id)) +│ ├── [child] dify.node.execution - "Start" (span_id = hash(node_exec_id_1)) +│ ├── [child] dify.node.execution - "LLM" (span_id = hash(node_exec_id_2)) +│ └── [child] dify.node.execution - "End" (span_id = hash(node_exec_id_3)) +``` + +### Scenario B: Nested Sub-Workflow +A workflow calling another workflow via a Tool or Sub-workflow node. The child workflow's spans are linked to the parent via `parent_span_id`. Both workflows share the same trace_id. + +``` +trace_id = UUID(outer_workflow_run_id) ← shared across both workflows +├── [root] dify.workflow.run (outer) (span_id = hash(outer_workflow_run_id)) +│ ├── dify.node.execution - "Start Node" +│ ├── dify.node.execution - "Tool Node" (triggers sub-workflow) +│ │ └── [child] dify.workflow.run (inner) (span_id = hash(inner_workflow_run_id)) +│ │ ├── dify.node.execution - "Inner Start" +│ │ └── dify.node.execution - "Inner End" +│ └── dify.node.execution - "End Node" +``` + +**Key attributes for nested workflows:** +- Inner workflow's `dify.parent.trace_id` = outer `workflow_run_id` +- Inner workflow's `dify.parent.node.execution_id` = tool node's `execution_id` +- Inner workflow's `dify.parent.workflow.run_id` = outer `workflow_run_id` +- Inner workflow's `dify.parent.app.id` = outer `app_id` + +### Scenario C: Draft Node Execution +A single node run in isolation (debugger/preview mode). It creates its own trace where the node span is the root. + +``` +trace_id = UUID(node_execution_id) ← own trace, NOT part of any workflow +└── dify.node.execution.draft (span_id = hash(node_execution_id)) +``` + +**Key difference:** Draft executions use `node_execution_id` as the correlation_id, so they are NOT children of any workflow trace. + +## Content Gating + +When `ENTERPRISE_INCLUDE_CONTENT` is set to `false`, sensitive content attributes (inputs, outputs, queries) are replaced with reference strings (e.g., `ref:workflow_run_id=...`) to prevent data leakage to the OTEL collector. + +**Reference String Format:** + +``` +ref:{id_type}={uuid} +``` + +**Examples:** + +``` +ref:workflow_run_id=550e8400-e29b-41d4-a716-446655440000 +ref:node_execution_id=660e8400-e29b-41d4-a716-446655440001 +ref:message_id=770e8400-e29b-41d4-a716-446655440002 +``` + +To retrieve actual content when gating is enabled, query the Dify database using the provided UUID. + +## Reference + +For a complete list of telemetry signals, attributes, and data structures, see [DATA_DICTIONARY.md](./DATA_DICTIONARY.md). diff --git a/api/enterprise/telemetry/contracts.py b/api/enterprise/telemetry/contracts.py index ac4cdeb323..91398cb8cb 100644 --- a/api/enterprise/telemetry/contracts.py +++ b/api/enterprise/telemetry/contracts.py @@ -9,7 +9,7 @@ from __future__ import annotations from enum import StrEnum from typing import Any -from pydantic import BaseModel, field_validator +from pydantic import BaseModel, ConfigDict class TelemetryCase(StrEnum): @@ -57,27 +57,17 @@ class TelemetryEnvelope(BaseModel): case: The telemetry case type. tenant_id: The tenant identifier. event_id: Unique event identifier for deduplication. - payload: The main event payload. - payload_fallback: Fallback payload (max 64KB). - metadata: Optional metadata dictionary. + payload: The main event payload (inline for small payloads, + empty when offloaded to storage via ``payload_ref``). + metadata: Optional metadata dictionary. When the gateway + offloads a large payload to object storage, this contains + ``{"payload_ref": ""}``. """ + model_config = ConfigDict(extra="forbid", use_enum_values=False) + case: TelemetryCase tenant_id: str event_id: str payload: dict[str, Any] - payload_fallback: bytes | None = None metadata: dict[str, Any] | None = None - - @field_validator("payload_fallback") - @classmethod - def validate_payload_fallback_size(cls, v: bytes | None) -> bytes | None: - """Validate that payload_fallback does not exceed 64KB.""" - if v is not None and len(v) > 65536: # 64 * 1024 - raise ValueError("payload_fallback must not exceed 64KB") - return v - - class Config: - """Pydantic configuration.""" - - use_enum_values = False diff --git a/api/enterprise/telemetry/enterprise_trace.py b/api/enterprise/telemetry/enterprise_trace.py index a6893c7c88..c9eeccbcea 100644 --- a/api/enterprise/telemetry/enterprise_trace.py +++ b/api/enterprise/telemetry/enterprise_trace.py @@ -102,7 +102,7 @@ class EnterpriseOtelTrace: 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.trace_id": trace_info.resolved_trace_id, "dify.tenant_id": tenant_id, "dify.app_id": app_id, "dify.app.name": metadata.get("app_name"), @@ -163,7 +163,7 @@ class EnterpriseOtelTrace: 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.trace_id": info.resolved_trace_id, "dify.tenant_id": tenant_id, "dify.app_id": app_id, "dify.workflow.id": info.workflow_id, @@ -177,8 +177,7 @@ class EnterpriseOtelTrace: "dify.invoked_by": info.invoked_by, } - trace_correlation_override: str | None = None - parent_span_id_source: str | None = None + trace_correlation_override, parent_span_id_source = info.resolved_parent_context parent_ctx = metadata.get("parent_trace_context") if isinstance(parent_ctx, dict): @@ -188,13 +187,6 @@ class EnterpriseOtelTrace: 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, @@ -307,7 +299,7 @@ class EnterpriseOtelTrace: 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.trace_id": info.resolved_trace_id, "dify.tenant_id": tenant_id, "dify.app_id": app_id, "dify.workflow.id": info.workflow_id, @@ -329,13 +321,8 @@ class EnterpriseOtelTrace: "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 + resolved_override, _ = info.resolved_parent_context + trace_correlation_override = trace_correlation_override_param or resolved_override effective_correlation_id = correlation_id_override or info.workflow_run_id self._exporter.export_span( @@ -419,9 +406,11 @@ class EnterpriseOtelTrace: **labels, type=request_type, status=info.status, + model_name=info.model_name or "", ), ) duration_labels = dict(labels) + duration_labels["model_name"] = info.model_name or "" plugin_name = metadata.get("plugin_name") if plugin_name and info.node_type in {"tool", "knowledge-retrieval"}: duration_labels["plugin_name"] = plugin_name @@ -434,6 +423,7 @@ class EnterpriseOtelTrace: self._labels( **labels, type=request_type, + model_name=info.model_name or "", ), ) @@ -488,15 +478,15 @@ class EnterpriseOtelTrace: 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", ""), + model_provider=metadata.get("ls_provider") or "", + model_name=metadata.get("ls_model_name") or "", ) token_labels = TokenMetricLabels( tenant_id=tenant_id or "", app_id=app_id or "", operation_type=OperationType.MESSAGE, - model_provider=metadata.get("ls_provider", ""), - model_name=metadata.get("ls_model_name", ""), + model_provider=metadata.get("ls_provider") or "", + model_name=metadata.get("ls_model_name") or "", node_type="", ).to_dict() self._exporter.increment_counter(EnterpriseTelemetryCounter.TOKENS, info.total_tokens, token_labels) @@ -560,6 +550,7 @@ class EnterpriseOtelTrace: emit_metric_only_event( event_name=EnterpriseTelemetryEvent.TOOL_EXECUTION, attributes=attrs, + trace_id_source=info.resolved_trace_id, span_id_source=node_execution_id, tenant_id=tenant_id, user_id=user_id, @@ -614,6 +605,7 @@ class EnterpriseOtelTrace: emit_metric_only_event( event_name=EnterpriseTelemetryEvent.MODERATION_CHECK, attributes=attrs, + trace_id_source=info.resolved_trace_id, span_id_source=node_execution_id, tenant_id=tenant_id, user_id=user_id, @@ -659,6 +651,7 @@ class EnterpriseOtelTrace: emit_metric_only_event( event_name=EnterpriseTelemetryEvent.SUGGESTED_QUESTION_GENERATION, attributes=attrs, + trace_id_source=info.resolved_trace_id, span_id_source=node_execution_id, tenant_id=tenant_id, user_id=user_id, @@ -674,6 +667,8 @@ class EnterpriseOtelTrace: self._labels( **labels, type="suggested_question", + model_provider=info.model_provider or "", + model_name=info.model_id or "", ), ) @@ -738,6 +733,13 @@ class EnterpriseOtelTrace: attrs["dify.dataset.embedding_providers"] = self._maybe_json(providers) attrs["dify.dataset.embedding_models"] = self._maybe_json(models) + # Add rerank model to logs + rerank_provider = metadata.get("rerank_model_provider", "") + rerank_model = metadata.get("rerank_model_name", "") + if rerank_provider or rerank_model: + attrs["dify.retrieval.rerank_provider"] = rerank_provider + attrs["dify.retrieval.rerank_model"] = rerank_model + 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) @@ -766,12 +768,25 @@ class EnterpriseOtelTrace: ) for did in dataset_ids: + # Get embedding model for this specific dataset + ds_embedding_info = embedding_models.get(did, {}) + embedding_provider = ds_embedding_info.get("embedding_model_provider", "") + embedding_model = ds_embedding_info.get("embedding_model", "") + + # Get rerank model (same for all datasets in this retrieval) + rerank_provider = metadata.get("rerank_model_provider", "") + rerank_model = metadata.get("rerank_model_name", "") + self._exporter.increment_counter( EnterpriseTelemetryCounter.DATASET_RETRIEVALS, 1, self._labels( **labels, dataset_id=did, + embedding_model_provider=embedding_provider, + embedding_model=embedding_model, + rerank_model_provider=rerank_provider, + rerank_model=rerank_model, ), ) @@ -793,6 +808,7 @@ class EnterpriseOtelTrace: emit_metric_only_event( event_name=EnterpriseTelemetryEvent.GENERATE_NAME_EXECUTION, attributes=attrs, + trace_id_source=info.resolved_trace_id, span_id_source=node_execution_id, tenant_id=tenant_id, user_id=user_id, @@ -815,7 +831,7 @@ class EnterpriseOtelTrace: metadata = self._metadata(info) tenant_id, app_id, user_id = self._context_ids(info, metadata) attrs = { - "dify.trace_id": info.trace_id, + "dify.trace_id": info.resolved_trace_id, "dify.tenant_id": tenant_id, "dify.user.id": user_id, "dify.app.id": app_id or "", @@ -846,6 +862,7 @@ class EnterpriseOtelTrace: emit_metric_only_event( event_name=EnterpriseTelemetryEvent.PROMPT_GENERATION_EXECUTION, attributes=attrs, + trace_id_source=info.resolved_trace_id, span_id_source=node_execution_id, tenant_id=tenant_id, user_id=user_id, diff --git a/api/enterprise/telemetry/event_handlers.py b/api/enterprise/telemetry/event_handlers.py index 38276c7f0f..167cde2cd8 100644 --- a/api/enterprise/telemetry/event_handlers.py +++ b/api/enterprise/telemetry/event_handlers.py @@ -1,13 +1,17 @@ """Blinker signal handlers for enterprise telemetry. Registered at import time via ``@signal.connect`` decorators. -Import must happen during ``ext_enterprise_telemetry.init_app()`` to ensure handlers fire. +Import must happen during ``ext_enterprise_telemetry.init_app()`` to +ensure handlers fire. Each handler delegates to ``core.telemetry.gateway`` +which handles routing, EE-gating, and dispatch. + +All handlers are best-effort: exceptions are caught and logged so that +telemetry failures never break user-facing operations. """ from __future__ import annotations import logging -import uuid from events.app_event import app_was_created, app_was_deleted, app_was_updated from events.feedback_event import feedback_was_created @@ -24,107 +28,72 @@ __all__ = [ @app_was_created.connect def _handle_app_created(sender: object, **kwargs: object) -> None: - from enterprise.telemetry.contracts import TelemetryCase, TelemetryEnvelope - from extensions.ext_enterprise_telemetry import get_enterprise_exporter - from tasks.enterprise_telemetry_task import process_enterprise_telemetry + try: + from core.telemetry.gateway import emit as gateway_emit + from enterprise.telemetry.contracts import TelemetryCase - exporter = get_enterprise_exporter() - if not exporter: - return - - tenant_id = str(getattr(sender, "tenant_id", "") or "") - payload = { - "app_id": getattr(sender, "id", None), - "mode": getattr(sender, "mode", None), - } - - envelope = TelemetryEnvelope( - case=TelemetryCase.APP_CREATED, - tenant_id=tenant_id, - event_id=str(uuid.uuid4()), - payload=payload, - ) - - process_enterprise_telemetry.delay(envelope.model_dump_json()) + gateway_emit( + case=TelemetryCase.APP_CREATED, + context={"tenant_id": str(getattr(sender, "tenant_id", "") or "")}, + payload={ + "app_id": getattr(sender, "id", None), + "mode": getattr(sender, "mode", None), + }, + ) + except Exception: + logger.warning("Failed to emit app_created telemetry", exc_info=True) @app_was_deleted.connect def _handle_app_deleted(sender: object, **kwargs: object) -> None: - from enterprise.telemetry.contracts import TelemetryCase, TelemetryEnvelope - from extensions.ext_enterprise_telemetry import get_enterprise_exporter - from tasks.enterprise_telemetry_task import process_enterprise_telemetry + try: + from core.telemetry.gateway import emit as gateway_emit + from enterprise.telemetry.contracts import TelemetryCase - exporter = get_enterprise_exporter() - if not exporter: - return - - tenant_id = str(getattr(sender, "tenant_id", "") or "") - payload = { - "app_id": getattr(sender, "id", None), - } - - envelope = TelemetryEnvelope( - case=TelemetryCase.APP_DELETED, - tenant_id=tenant_id, - event_id=str(uuid.uuid4()), - payload=payload, - ) - - process_enterprise_telemetry.delay(envelope.model_dump_json()) + gateway_emit( + case=TelemetryCase.APP_DELETED, + context={"tenant_id": str(getattr(sender, "tenant_id", "") or "")}, + payload={"app_id": getattr(sender, "id", None)}, + ) + except Exception: + logger.warning("Failed to emit app_deleted telemetry", exc_info=True) @app_was_updated.connect def _handle_app_updated(sender: object, **kwargs: object) -> None: - from enterprise.telemetry.contracts import TelemetryCase, TelemetryEnvelope - from extensions.ext_enterprise_telemetry import get_enterprise_exporter - from tasks.enterprise_telemetry_task import process_enterprise_telemetry + try: + from core.telemetry.gateway import emit as gateway_emit + from enterprise.telemetry.contracts import TelemetryCase - exporter = get_enterprise_exporter() - if not exporter: - return - - tenant_id = str(getattr(sender, "tenant_id", "") or "") - payload = { - "app_id": getattr(sender, "id", None), - } - - envelope = TelemetryEnvelope( - case=TelemetryCase.APP_UPDATED, - tenant_id=tenant_id, - event_id=str(uuid.uuid4()), - payload=payload, - ) - - process_enterprise_telemetry.delay(envelope.model_dump_json()) + gateway_emit( + case=TelemetryCase.APP_UPDATED, + context={"tenant_id": str(getattr(sender, "tenant_id", "") or "")}, + payload={"app_id": getattr(sender, "id", None)}, + ) + except Exception: + logger.warning("Failed to emit app_updated telemetry", exc_info=True) @feedback_was_created.connect def _handle_feedback_created(sender: object, **kwargs: object) -> None: - from enterprise.telemetry.contracts import TelemetryCase, TelemetryEnvelope - from extensions.ext_enterprise_telemetry import get_enterprise_exporter - from tasks.enterprise_telemetry_task import process_enterprise_telemetry + try: + from core.telemetry.gateway import emit as gateway_emit + from enterprise.telemetry.contracts import TelemetryCase - exporter = get_enterprise_exporter() - if not exporter: - return - - tenant_id = str(kwargs.get("tenant_id", "") or "") - payload = { - "message_id": getattr(sender, "message_id", None), - "app_id": getattr(sender, "app_id", None), - "conversation_id": getattr(sender, "conversation_id", None), - "from_end_user_id": getattr(sender, "from_end_user_id", None), - "from_account_id": getattr(sender, "from_account_id", None), - "rating": getattr(sender, "rating", None), - "from_source": getattr(sender, "from_source", None), - "content": getattr(sender, "content", None), - } - - envelope = TelemetryEnvelope( - case=TelemetryCase.FEEDBACK_CREATED, - tenant_id=tenant_id, - event_id=str(uuid.uuid4()), - payload=payload, - ) - - process_enterprise_telemetry.delay(envelope.model_dump_json()) + tenant_id = str(kwargs.get("tenant_id", "") or "") + gateway_emit( + case=TelemetryCase.FEEDBACK_CREATED, + context={"tenant_id": tenant_id}, + payload={ + "message_id": getattr(sender, "message_id", None), + "app_id": getattr(sender, "app_id", None), + "conversation_id": getattr(sender, "conversation_id", None), + "from_end_user_id": getattr(sender, "from_end_user_id", None), + "from_account_id": getattr(sender, "from_account_id", None), + "rating": getattr(sender, "rating", None), + "from_source": getattr(sender, "from_source", None), + "content": getattr(sender, "content", None), + }, + ) + except Exception: + logger.warning("Failed to emit feedback_created telemetry", exc_info=True) diff --git a/api/enterprise/telemetry/exporter.py b/api/enterprise/telemetry/exporter.py index 247e03691f..6276853dc1 100644 --- a/api/enterprise/telemetry/exporter.py +++ b/api/enterprise/telemetry/exporter.py @@ -109,8 +109,9 @@ class EnterpriseExporter: sampling_rate: float = getattr(config, "ENTERPRISE_OTEL_SAMPLING_RATE", 1.0) self.include_content: bool = getattr(config, "ENTERPRISE_INCLUDE_CONTENT", True) api_key: str = getattr(config, "ENTERPRISE_OTLP_API_KEY", "") - # Auto-detect TLS: when bearer token is configured, use secure channel - insecure: bool = not bool(api_key) + + # Auto-detect TLS: https:// uses secure, everything else is insecure + insecure = not endpoint.startswith("https://") resource = Resource( attributes={ @@ -206,7 +207,15 @@ class EnterpriseExporter: if parent_span_id_source: # Cross-workflow linking: parent is an explicit span (e.g. tool node in outer workflow) parent_span_id = compute_deterministic_span_id(parent_span_id_source) - parent_trace_id = int(uuid.UUID(effective_trace_correlation)) if effective_trace_correlation else 0 + try: + parent_trace_id = int(uuid.UUID(effective_trace_correlation)) if effective_trace_correlation else 0 + except (ValueError, AttributeError): + logger.warning( + "Invalid trace correlation UUID for cross-workflow link: %s, span=%s", + effective_trace_correlation, + name, + ) + parent_trace_id = 0 if parent_trace_id: parent_span_context = SpanContext( trace_id=parent_trace_id, @@ -218,14 +227,23 @@ class EnterpriseExporter: elif correlation_id and correlation_id != span_id_source: # Child span: parent is the correlation-group root (workflow root span) parent_span_id = compute_deterministic_span_id(correlation_id) - parent_trace_id = int(uuid.UUID(effective_trace_correlation or correlation_id)) - parent_span_context = SpanContext( - trace_id=parent_trace_id, - span_id=parent_span_id, - is_remote=True, - trace_flags=TraceFlags(TraceFlags.SAMPLED), - ) - parent_context = trace.set_span_in_context(trace.NonRecordingSpan(parent_span_context)) + try: + parent_trace_id = int(uuid.UUID(effective_trace_correlation or correlation_id)) + except (ValueError, AttributeError): + logger.warning( + "Invalid trace correlation UUID for child span link: %s, span=%s", + effective_trace_correlation or correlation_id, + name, + ) + parent_trace_id = 0 + if parent_trace_id: + parent_span_context = SpanContext( + trace_id=parent_trace_id, + span_id=parent_span_id, + is_remote=True, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ) + parent_context = trace.set_span_in_context(trace.NonRecordingSpan(parent_span_context)) span_start_time = _datetime_to_ns(start_time) if start_time is not None else None span_end_on_exit = end_time is None diff --git a/api/enterprise/telemetry/gateway.py b/api/enterprise/telemetry/gateway.py deleted file mode 100644 index 73886e327e..0000000000 --- a/api/enterprise/telemetry/gateway.py +++ /dev/null @@ -1,199 +0,0 @@ -"""Telemetry gateway routing and dispatch. - -Maps ``TelemetryCase`` → ``CaseRoute`` (signal type + CE eligibility) -and dispatches events to either the trace pipeline or the metric/log -Celery queue. - -Singleton lifecycle is managed by ``ext_enterprise_telemetry.init_app()`` -which creates the instance during single-threaded Flask app startup. -Access via ``ext_enterprise_telemetry.get_gateway()``. -""" - -from __future__ import annotations - -import json -import logging -import uuid -from typing import TYPE_CHECKING, Any - -from core.ops.entities.trace_entity import TraceTaskName -from enterprise.telemetry.contracts import CaseRoute, SignalType, TelemetryCase, TelemetryEnvelope -from extensions.ext_storage import storage - -if TYPE_CHECKING: - from core.ops.ops_trace_manager import TraceQueueManager - -logger = logging.getLogger(__name__) - -PAYLOAD_SIZE_THRESHOLD_BYTES = 1 * 1024 * 1024 - -CASE_TO_TRACE_TASK: dict[TelemetryCase, TraceTaskName] = { - TelemetryCase.WORKFLOW_RUN: TraceTaskName.WORKFLOW_TRACE, - TelemetryCase.MESSAGE_RUN: TraceTaskName.MESSAGE_TRACE, - TelemetryCase.NODE_EXECUTION: TraceTaskName.NODE_EXECUTION_TRACE, - TelemetryCase.DRAFT_NODE_EXECUTION: TraceTaskName.DRAFT_NODE_EXECUTION_TRACE, - TelemetryCase.PROMPT_GENERATION: TraceTaskName.PROMPT_GENERATION_TRACE, -} - -CASE_ROUTING: dict[TelemetryCase, CaseRoute] = { - TelemetryCase.WORKFLOW_RUN: CaseRoute(signal_type=SignalType.TRACE, ce_eligible=True), - TelemetryCase.MESSAGE_RUN: CaseRoute(signal_type=SignalType.TRACE, ce_eligible=True), - TelemetryCase.NODE_EXECUTION: CaseRoute(signal_type=SignalType.TRACE, ce_eligible=False), - TelemetryCase.DRAFT_NODE_EXECUTION: CaseRoute(signal_type=SignalType.TRACE, ce_eligible=False), - TelemetryCase.PROMPT_GENERATION: CaseRoute(signal_type=SignalType.TRACE, ce_eligible=False), - TelemetryCase.APP_CREATED: CaseRoute(signal_type=SignalType.METRIC_LOG, ce_eligible=False), - TelemetryCase.APP_UPDATED: CaseRoute(signal_type=SignalType.METRIC_LOG, ce_eligible=False), - TelemetryCase.APP_DELETED: CaseRoute(signal_type=SignalType.METRIC_LOG, ce_eligible=False), - TelemetryCase.FEEDBACK_CREATED: CaseRoute(signal_type=SignalType.METRIC_LOG, ce_eligible=False), - TelemetryCase.TOOL_EXECUTION: CaseRoute(signal_type=SignalType.METRIC_LOG, ce_eligible=False), - TelemetryCase.MODERATION_CHECK: CaseRoute(signal_type=SignalType.METRIC_LOG, ce_eligible=False), - TelemetryCase.SUGGESTED_QUESTION: CaseRoute(signal_type=SignalType.METRIC_LOG, ce_eligible=False), - TelemetryCase.DATASET_RETRIEVAL: CaseRoute(signal_type=SignalType.METRIC_LOG, ce_eligible=False), - TelemetryCase.GENERATE_NAME: CaseRoute(signal_type=SignalType.METRIC_LOG, ce_eligible=False), -} - - -def _is_enterprise_telemetry_enabled() -> bool: - try: - from enterprise.telemetry.exporter import is_enterprise_telemetry_enabled - - return is_enterprise_telemetry_enabled() - except Exception: - return False - - -def _should_drop_ee_only_event(route: CaseRoute) -> bool: - """Return True when the event is enterprise-only and EE telemetry is disabled.""" - return not route.ce_eligible and not _is_enterprise_telemetry_enabled() - - -class TelemetryGateway: - """Routes telemetry events to the trace pipeline or the metric/log Celery queue. - - Stateless — instantiated once during ``ext_enterprise_telemetry.init_app()`` - and shared for the lifetime of the process. - """ - - def emit( - self, - case: TelemetryCase, - context: dict[str, Any], - payload: dict[str, Any], - trace_manager: TraceQueueManager | None = None, - ) -> None: - route = CASE_ROUTING.get(case) - if route is None: - logger.warning("Unknown telemetry case: %s, dropping event", case) - return - - if _should_drop_ee_only_event(route): - logger.debug("Dropping EE-only event: case=%s (EE disabled)", case) - return - - logger.debug( - "Gateway routing: case=%s, signal_type=%s, ce_eligible=%s", - case, - route.signal_type, - route.ce_eligible, - ) - - if route.signal_type is SignalType.TRACE: - self._emit_trace(case, context, payload, route, trace_manager) - else: - self._emit_metric_log(case, context, payload) - - def _emit_trace( - self, - case: TelemetryCase, - context: dict[str, Any], - payload: dict[str, Any], - route: CaseRoute, - trace_manager: TraceQueueManager | None, - ) -> None: - from core.ops.ops_trace_manager import TraceQueueManager as LocalTraceQueueManager - from core.ops.ops_trace_manager import TraceTask - - trace_task_name = CASE_TO_TRACE_TASK.get(case) - if trace_task_name is None: - logger.warning("No TraceTaskName mapping for case: %s", case) - return - - queue_manager = trace_manager or LocalTraceQueueManager( - app_id=context.get("app_id"), - user_id=context.get("user_id"), - ) - - queue_manager.add_trace_task(TraceTask(trace_task_name, **payload)) - logger.debug("Enqueued trace task: case=%s, app_id=%s", case, context.get("app_id")) - - def _emit_metric_log( - self, - case: TelemetryCase, - context: dict[str, Any], - payload: dict[str, Any], - ) -> None: - from tasks.enterprise_telemetry_task import process_enterprise_telemetry - - tenant_id = context.get("tenant_id", "") - event_id = str(uuid.uuid4()) - - payload_for_envelope, payload_ref = self._handle_payload_sizing(payload, tenant_id, event_id) - - envelope = TelemetryEnvelope( - case=case, - tenant_id=tenant_id, - event_id=event_id, - payload=payload_for_envelope, - metadata={"payload_ref": payload_ref} if payload_ref else None, - ) - - process_enterprise_telemetry.delay(envelope.model_dump_json()) - logger.debug( - "Enqueued metric/log event: case=%s, tenant_id=%s, event_id=%s", - case, - tenant_id, - event_id, - ) - - def _handle_payload_sizing( - self, - payload: dict[str, Any], - tenant_id: str, - event_id: str, - ) -> tuple[dict[str, Any], str | None]: - try: - payload_json = json.dumps(payload) - payload_size = len(payload_json.encode("utf-8")) - except (TypeError, ValueError): - logger.warning("Failed to serialize payload for sizing: event_id=%s", event_id) - return payload, None - - if payload_size <= PAYLOAD_SIZE_THRESHOLD_BYTES: - return payload, None - - storage_key = f"telemetry/{tenant_id}/{event_id}.json" - try: - storage.save(storage_key, payload_json.encode("utf-8")) - logger.debug("Stored large payload to storage: key=%s, size=%d", storage_key, payload_size) - return {}, storage_key - except Exception: - logger.warning("Failed to store large payload, inlining instead: event_id=%s", event_id, exc_info=True) - return payload, None - - -def emit( - case: TelemetryCase, - context: dict[str, Any], - payload: dict[str, Any], - trace_manager: TraceQueueManager | None = None, -) -> None: - """Module-level convenience wrapper. - - Fetches the gateway singleton from the extension; no-ops when - enterprise telemetry is disabled (gateway is ``None``). - """ - from extensions.ext_enterprise_telemetry import get_gateway - - gateway = get_gateway() - if gateway is not None: - gateway.emit(case, context, payload, trace_manager) diff --git a/api/enterprise/telemetry/metric_handler.py b/api/enterprise/telemetry/metric_handler.py index 381dca3350..25bec993b7 100644 --- a/api/enterprise/telemetry/metric_handler.py +++ b/api/enterprise/telemetry/metric_handler.py @@ -7,11 +7,13 @@ idempotency checking, and payload rehydration. from __future__ import annotations +import json import logging from typing import Any from enterprise.telemetry.contracts import TelemetryCase, TelemetryEnvelope from extensions.ext_redis import redis_client +from extensions.ext_storage import storage logger = logging.getLogger(__name__) @@ -136,44 +138,46 @@ class EnterpriseMetricHandler: return False def _rehydrate(self, envelope: TelemetryEnvelope) -> dict[str, Any]: - """Rehydrate payload from reference or fallback. + """Rehydrate payload from storage reference or inline data. - Attempts to resolve payload_ref to full data. If that fails, - falls back to payload_fallback. If both fail, emits a degraded - event marker. + If the envelope payload is empty and metadata contains a + ``payload_ref``, the full payload is loaded from object storage + (where the gateway wrote it as JSON). When both the inline + payload and storage resolution fail, a degraded-event marker + is emitted so the gap is observable. Args: envelope: The telemetry envelope containing payload data. Returns: - The rehydrated payload dictionary. + The rehydrated payload dictionary, or ``{}`` on total failure. """ - # For now, payload is directly in the envelope - # Future: implement payload_ref resolution from storage payload = envelope.payload - if not payload and envelope.payload_fallback: - import pickle - - try: - payload = pickle.loads(envelope.payload_fallback) # noqa: S301 - logger.debug("Used payload_fallback for event_id=%s", envelope.event_id) - except Exception: - logger.warning( - "Failed to deserialize payload_fallback for event_id=%s", - envelope.event_id, - exc_info=True, - ) + # Resolve from object storage when the gateway offloaded a large payload. + if not payload and envelope.metadata: + payload_ref = envelope.metadata.get("payload_ref") + if payload_ref: + try: + payload_bytes = storage.load(payload_ref) + payload = json.loads(payload_bytes.decode("utf-8")) + logger.debug("Loaded payload from storage: key=%s", payload_ref) + except Exception: + logger.warning( + "Failed to load payload from storage: key=%s, event_id=%s", + payload_ref, + envelope.event_id, + exc_info=True, + ) if not payload: - # Both ref and fallback failed - emit degraded event + # Storage resolution failed or no data available — emit degraded event. logger.error( "Payload rehydration failed for event_id=%s, tenant_id=%s, case=%s", envelope.event_id, envelope.tenant_id, envelope.case, ) - # Emit degraded event marker from enterprise.telemetry.entities import EnterpriseTelemetryEvent from enterprise.telemetry.telemetry_log import emit_metric_only_event diff --git a/api/extensions/ext_celery.py b/api/extensions/ext_celery.py index af983f6d87..9944e768b9 100644 --- a/api/extensions/ext_celery.py +++ b/api/extensions/ext_celery.py @@ -184,6 +184,8 @@ def init_app(app: DifyApp) -> Celery: "task": "schedule.trigger_provider_refresh_task.trigger_provider_refresh", "schedule": timedelta(minutes=dify_config.TRIGGER_PROVIDER_REFRESH_INTERVAL), } + if dify_config.ENTERPRISE_TELEMETRY_ENABLED: + imports.append("tasks.enterprise_telemetry_task") celery_app.conf.update(beat_schedule=beat_schedule, imports=imports) return celery_app diff --git a/api/extensions/ext_enterprise_telemetry.py b/api/extensions/ext_enterprise_telemetry.py index a24e14efa7..f785c00ae0 100644 --- a/api/extensions/ext_enterprise_telemetry.py +++ b/api/extensions/ext_enterprise_telemetry.py @@ -1,8 +1,8 @@ """Flask extension for enterprise telemetry lifecycle management. -Initializes the EnterpriseExporter and TelemetryGateway singletons during -``create_app()`` (single-threaded), registers blinker event handlers, -and hooks atexit for graceful shutdown. +Initializes the EnterpriseExporter singleton during ``create_app()`` +(single-threaded), registers blinker event handlers, and hooks atexit +for graceful shutdown. Skipped entirely when ``ENTERPRISE_ENABLED`` and ``ENTERPRISE_TELEMETRY_ENABLED`` are false (``is_enabled()`` gate). @@ -19,12 +19,10 @@ from configs import dify_config if TYPE_CHECKING: from dify_app import DifyApp from enterprise.telemetry.exporter import EnterpriseExporter - from enterprise.telemetry.gateway import TelemetryGateway logger = logging.getLogger(__name__) _exporter: EnterpriseExporter | None = None -_gateway: TelemetryGateway | None = None def is_enabled() -> bool: @@ -32,16 +30,14 @@ def is_enabled() -> bool: def init_app(app: DifyApp) -> None: - global _exporter, _gateway + global _exporter if not is_enabled(): return from enterprise.telemetry.exporter import EnterpriseExporter - from enterprise.telemetry.gateway import TelemetryGateway _exporter = EnterpriseExporter(dify_config) - _gateway = TelemetryGateway() atexit.register(_exporter.shutdown) # Import to trigger @signal.connect decorator registration @@ -52,7 +48,3 @@ def init_app(app: DifyApp) -> None: def get_enterprise_exporter() -> EnterpriseExporter | None: return _exporter - - -def get_gateway() -> TelemetryGateway | None: - return _gateway diff --git a/api/extensions/ext_otel.py b/api/extensions/ext_otel.py index 40a915e68c..37f881f7ea 100644 --- a/api/extensions/ext_otel.py +++ b/api/extensions/ext_otel.py @@ -59,16 +59,24 @@ def init_app(app: DifyApp): protocol = (dify_config.OTEL_EXPORTER_OTLP_PROTOCOL or "").lower() if dify_config.OTEL_EXPORTER_TYPE == "otlp": if protocol == "grpc": + # Auto-detect TLS: https:// uses secure, everything else is insecure + endpoint = dify_config.OTLP_BASE_ENDPOINT + insecure = not endpoint.startswith("https://") + exporter = GRPCSpanExporter( - endpoint=dify_config.OTLP_BASE_ENDPOINT, + endpoint=endpoint, # Header field names must consist of lowercase letters, check RFC7540 - headers=(("authorization", f"Bearer {dify_config.OTLP_API_KEY}"),), - insecure=True, + headers=( + (("authorization", f"Bearer {dify_config.OTLP_API_KEY}"),) if dify_config.OTLP_API_KEY else None + ), + insecure=insecure, ) metric_exporter = GRPCMetricExporter( - endpoint=dify_config.OTLP_BASE_ENDPOINT, - headers=(("authorization", f"Bearer {dify_config.OTLP_API_KEY}"),), - insecure=True, + endpoint=endpoint, + headers=( + (("authorization", f"Bearer {dify_config.OTLP_API_KEY}"),) if dify_config.OTLP_API_KEY else None + ), + insecure=insecure, ) else: headers = {"Authorization": f"Bearer {dify_config.OTLP_API_KEY}"} if dify_config.OTLP_API_KEY else None diff --git a/api/models/model.py b/api/models/model.py index 429c46bd85..1a259e4621 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -664,15 +664,11 @@ class ExporleBanner(TypeBase): content: Mapped[dict[str, Any]] = mapped_column(sa.JSON, nullable=False) link: Mapped[str] = mapped_column(String(255), nullable=False) sort: Mapped[int] = mapped_column(sa.Integer, nullable=False) - status: Mapped[str] = mapped_column( - sa.String(255), nullable=False, server_default='enabled', default="enabled" - ) + status: Mapped[str] = mapped_column(sa.String(255), nullable=False, server_default="enabled", default="enabled") created_at: Mapped[datetime] = mapped_column( sa.DateTime, nullable=False, server_default=func.current_timestamp(), init=False ) - language: Mapped[str] = mapped_column( - String(255), nullable=False, server_default='en-US', default="en-US" - ) + language: Mapped[str] = mapped_column(String(255), nullable=False, server_default="en-US", default="en-US") class OAuthProviderApp(TypeBase): diff --git a/api/pyproject.toml b/api/pyproject.toml index 2a7c946e6e..e975b55f63 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -22,14 +22,14 @@ dependencies = [ "flask-sqlalchemy~=3.1.1", "gevent~=25.9.1", "gmpy2~=2.2.1", - "google-api-core==2.18.0", + "google-api-core>=2.19.1", "google-api-python-client==2.90.0", - "google-auth==2.29.0", + "google-auth>=2.47.0", "google-auth-httplib2==0.2.0", - "google-cloud-aiplatform==1.49.0", - "googleapis-common-protos==1.63.0", + "google-cloud-aiplatform>=1.123.0", + "googleapis-common-protos>=1.65.0", "gunicorn~=23.0.0", - "httpx[socks]~=0.27.0", + "httpx[socks]~=0.28.0", "jieba==0.42.1", "json-repair>=0.55.1", "jsonschema>=4.25.1", @@ -41,26 +41,23 @@ dependencies = [ "openpyxl~=3.1.5", "opik~=1.8.72", "litellm==1.77.1", # Pinned to avoid madoka dependency issue - "opentelemetry-api==1.27.0", - "opentelemetry-distro==0.48b0", - "opentelemetry-exporter-otlp==1.27.0", - "opentelemetry-exporter-otlp-proto-common==1.27.0", - "opentelemetry-exporter-otlp-proto-grpc==1.27.0", - "opentelemetry-exporter-otlp-proto-http==1.27.0", - "opentelemetry-instrumentation==0.48b0", - "opentelemetry-instrumentation-celery==0.48b0", - "opentelemetry-instrumentation-flask==0.48b0", - "opentelemetry-instrumentation-httpx==0.48b0", - "opentelemetry-instrumentation-redis==0.48b0", - "opentelemetry-instrumentation-httpx==0.48b0", - "opentelemetry-instrumentation-sqlalchemy==0.48b0", - "opentelemetry-propagator-b3==1.27.0", - # opentelemetry-proto1.28.0 depends on protobuf (>=5.0,<6.0), - # which is conflict with googleapis-common-protos (1.63.0) - "opentelemetry-proto==1.27.0", - "opentelemetry-sdk==1.27.0", - "opentelemetry-semantic-conventions==0.48b0", - "opentelemetry-util-http==0.48b0", + "opentelemetry-api==1.28.0", + "opentelemetry-distro==0.49b0", + "opentelemetry-exporter-otlp==1.28.0", + "opentelemetry-exporter-otlp-proto-common==1.28.0", + "opentelemetry-exporter-otlp-proto-grpc==1.28.0", + "opentelemetry-exporter-otlp-proto-http==1.28.0", + "opentelemetry-instrumentation==0.49b0", + "opentelemetry-instrumentation-celery==0.49b0", + "opentelemetry-instrumentation-flask==0.49b0", + "opentelemetry-instrumentation-httpx==0.49b0", + "opentelemetry-instrumentation-redis==0.49b0", + "opentelemetry-instrumentation-sqlalchemy==0.49b0", + "opentelemetry-propagator-b3==1.28.0", + "opentelemetry-proto==1.28.0", + "opentelemetry-sdk==1.28.0", + "opentelemetry-semantic-conventions==0.49b0", + "opentelemetry-util-http==0.49b0", "pandas[excel,output-formatting,performance]~=2.2.2", "psycogreen~=1.0.2", "psycopg2-binary~=2.9.6", diff --git a/api/services/enterprise/account_deletion_sync.py b/api/services/enterprise/account_deletion_sync.py index f8f8189891..c7ff42894d 100644 --- a/api/services/enterprise/account_deletion_sync.py +++ b/api/services/enterprise/account_deletion_sync.py @@ -81,7 +81,7 @@ def sync_workspace_member_removal(workspace_id: str, member_id: str, *, source: bool: True if task was queued (or skipped in community), False if queueing failed """ if not dify_config.ENTERPRISE_ENABLED: - return True + return True return _queue_task(workspace_id=workspace_id, member_id=member_id, source=source) @@ -101,7 +101,7 @@ def sync_account_deletion(account_id: str, *, source: str) -> bool: bool: True if all tasks were queued (or skipped in community), False if any queueing failed """ if not dify_config.ENTERPRISE_ENABLED: - return True + return True # Fetch all workspaces the account belongs to workspace_joins = db.session.query(TenantAccountJoin).filter_by(account_id=account_id).all() diff --git a/api/tasks/ops_trace_task.py b/api/tasks/ops_trace_task.py index 5b61e9e7a1..3d3a9755a5 100644 --- a/api/tasks/ops_trace_task.py +++ b/api/tasks/ops_trace_task.py @@ -51,7 +51,7 @@ def process_trace_tasks(file_info): try: EnterpriseOtelTrace().trace(trace_info) except Exception: - logger.warning("Enterprise trace failed for app_id: %s", app_id, exc_info=True) + logger.exception("Enterprise trace failed for app_id: %s", app_id) if trace_instance: with current_app.app_context(): diff --git a/api/tests/unit_tests/core/ops/test_trace_queue_manager.py b/api/tests/unit_tests/core/ops/test_trace_queue_manager.py index 25adda21ec..44a58ab902 100644 --- a/api/tests/unit_tests/core/ops/test_trace_queue_manager.py +++ b/api/tests/unit_tests/core/ops/test_trace_queue_manager.py @@ -39,7 +39,7 @@ def trace_queue_manager_and_task(monkeypatch): class StubTraceQueueManager: def __init__(self, app_id=None): self.app_id = app_id - from core.telemetry import is_enterprise_telemetry_enabled + from core.telemetry.gateway import is_enterprise_telemetry_enabled self._enterprise_telemetry_enabled = is_enterprise_telemetry_enabled() self.trace_instance = StubOpsTraceManager.get_ops_trace_instance(app_id) @@ -87,7 +87,7 @@ class TestTraceQueueManagerTelemetryGuard: trace_task = TraceTask(trace_type=TraceTaskName.WORKFLOW_TRACE) with ( - patch("core.telemetry.is_enterprise_telemetry_enabled", return_value=False), + patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=False), patch("core.ops.ops_trace_manager.OpsTraceManager.get_ops_trace_instance", return_value=None), patch("core.ops.ops_trace_manager.trace_manager_queue", mock_queue), ): @@ -109,7 +109,7 @@ class TestTraceQueueManagerTelemetryGuard: trace_task = TraceTask(trace_type=TraceTaskName.WORKFLOW_TRACE) with ( - patch("core.telemetry.is_enterprise_telemetry_enabled", return_value=True), + patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=True), patch("core.ops.ops_trace_manager.OpsTraceManager.get_ops_trace_instance", return_value=None), patch("core.ops.ops_trace_manager.trace_manager_queue", mock_queue), ): @@ -135,7 +135,7 @@ class TestTraceQueueManagerTelemetryGuard: trace_task = TraceTask(trace_type=TraceTaskName.WORKFLOW_TRACE) with ( - patch("core.telemetry.is_enterprise_telemetry_enabled", return_value=False), + patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=False), patch( "core.ops.ops_trace_manager.OpsTraceManager.get_ops_trace_instance", return_value=mock_trace_instance ), @@ -163,7 +163,7 @@ class TestTraceQueueManagerTelemetryGuard: trace_task = TraceTask(trace_type=TraceTaskName.WORKFLOW_TRACE) with ( - patch("core.telemetry.is_enterprise_telemetry_enabled", return_value=True), + patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=True), patch( "core.ops.ops_trace_manager.OpsTraceManager.get_ops_trace_instance", return_value=mock_trace_instance ), @@ -189,7 +189,7 @@ class TestTraceQueueManagerTelemetryGuard: trace_task = TraceTask(trace_type=TraceTaskName.WORKFLOW_TRACE) with ( - patch("core.telemetry.is_enterprise_telemetry_enabled", return_value=True), + patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=True), patch("core.ops.ops_trace_manager.OpsTraceManager.get_ops_trace_instance", return_value=None), patch("core.ops.ops_trace_manager.trace_manager_queue", mock_queue), ): diff --git a/api/tests/unit_tests/core/telemetry/test_facade.py b/api/tests/unit_tests/core/telemetry/test_facade.py index ae7b2ce818..64c2f6a971 100644 --- a/api/tests/unit_tests/core/telemetry/test_facade.py +++ b/api/tests/unit_tests/core/telemetry/test_facade.py @@ -53,7 +53,7 @@ def telemetry_test_setup(monkeypatch): class TestTelemetryEmit: - @patch("core.telemetry._is_enterprise_telemetry_enabled", return_value=True) + @patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=True) def test_emit_enterprise_trace_creates_trace_task(self, _mock_ee, telemetry_test_setup): emit_fn, mock_queue = telemetry_test_setup @@ -107,7 +107,7 @@ class TestTelemetryEmit: mock_queue.put.assert_not_called() - @patch("core.telemetry._is_enterprise_telemetry_enabled", return_value=True) + @patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=True) def test_emit_all_enterprise_only_traces_allowed_when_ee_enabled(self, _mock_ee, telemetry_test_setup): emit_fn, mock_queue = telemetry_test_setup @@ -136,7 +136,7 @@ class TestTelemetryEmit: called_task = mock_queue.put.call_args[0][0] assert called_task.trace_type == trace_name - @patch("core.telemetry._is_enterprise_telemetry_enabled", return_value=True) + @patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=True) def test_emit_passes_name_directly_to_trace_task(self, _mock_ee, telemetry_test_setup): emit_fn, mock_queue = telemetry_test_setup @@ -157,7 +157,7 @@ class TestTelemetryEmit: assert called_task.trace_type == TraceTaskName.DRAFT_NODE_EXECUTION_TRACE assert isinstance(called_task.trace_type, TraceTaskName) - @patch("core.telemetry._is_enterprise_telemetry_enabled", return_value=True) + @patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=True) def test_emit_with_provided_trace_manager(self, _mock_ee, telemetry_test_setup): emit_fn, mock_queue = telemetry_test_setup diff --git a/api/tests/unit_tests/core/telemetry/test_gateway_integration.py b/api/tests/unit_tests/core/telemetry/test_gateway_integration.py index 076cd00879..536d4374d6 100644 --- a/api/tests/unit_tests/core/telemetry/test_gateway_integration.py +++ b/api/tests/unit_tests/core/telemetry/test_gateway_integration.py @@ -5,14 +5,13 @@ from unittest.mock import MagicMock, patch import pytest -from core.telemetry import is_enterprise_telemetry_enabled +from core.telemetry.gateway import emit, is_enterprise_telemetry_enabled from enterprise.telemetry.contracts import TelemetryCase -from enterprise.telemetry.gateway import TelemetryGateway class TestTelemetryCoreExports: def test_is_enterprise_telemetry_enabled_exported(self) -> None: - from core.telemetry import is_enterprise_telemetry_enabled as exported_func + from core.telemetry.gateway import is_enterprise_telemetry_enabled as exported_func assert callable(exported_func) @@ -38,10 +37,6 @@ def mock_ops_trace_manager(): class TestGatewayIntegrationTraceRouting: - @pytest.fixture - def gateway(self) -> TelemetryGateway: - return TelemetryGateway() - @pytest.fixture def mock_trace_manager(self) -> MagicMock: return MagicMock() @@ -49,68 +44,61 @@ class TestGatewayIntegrationTraceRouting: @pytest.mark.usefixtures("mock_ops_trace_manager") def test_ce_eligible_trace_routed_to_trace_manager( self, - gateway: TelemetryGateway, mock_trace_manager: MagicMock, ) -> None: - with patch("enterprise.telemetry.gateway._is_enterprise_telemetry_enabled", return_value=True): + with patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=True): context = {"app_id": "app-123", "user_id": "user-456", "tenant_id": "tenant-789"} payload = {"workflow_run_id": "run-abc"} - gateway.emit(TelemetryCase.WORKFLOW_RUN, context, payload, mock_trace_manager) + emit(TelemetryCase.WORKFLOW_RUN, context, payload, mock_trace_manager) mock_trace_manager.add_trace_task.assert_called_once() @pytest.mark.usefixtures("mock_ops_trace_manager") def test_ce_eligible_trace_routed_when_ee_disabled( self, - gateway: TelemetryGateway, mock_trace_manager: MagicMock, ) -> None: - with patch("enterprise.telemetry.gateway._is_enterprise_telemetry_enabled", return_value=False): + with patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=False): context = {"app_id": "app-123", "user_id": "user-456"} payload = {"workflow_run_id": "run-abc"} - gateway.emit(TelemetryCase.WORKFLOW_RUN, context, payload, mock_trace_manager) + emit(TelemetryCase.WORKFLOW_RUN, context, payload, mock_trace_manager) mock_trace_manager.add_trace_task.assert_called_once() @pytest.mark.usefixtures("mock_ops_trace_manager") def test_enterprise_only_trace_dropped_when_ee_disabled( self, - gateway: TelemetryGateway, mock_trace_manager: MagicMock, ) -> None: - with patch("enterprise.telemetry.gateway._is_enterprise_telemetry_enabled", return_value=False): + with patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=False): context = {"app_id": "app-123", "user_id": "user-456"} payload = {"node_id": "node-abc"} - gateway.emit(TelemetryCase.NODE_EXECUTION, context, payload, mock_trace_manager) + emit(TelemetryCase.NODE_EXECUTION, context, payload, mock_trace_manager) mock_trace_manager.add_trace_task.assert_not_called() @pytest.mark.usefixtures("mock_ops_trace_manager") def test_enterprise_only_trace_routed_when_ee_enabled( self, - gateway: TelemetryGateway, mock_trace_manager: MagicMock, ) -> None: - with patch("enterprise.telemetry.gateway._is_enterprise_telemetry_enabled", return_value=True): + with patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=True): context = {"app_id": "app-123", "user_id": "user-456"} payload = {"node_id": "node-abc"} - gateway.emit(TelemetryCase.NODE_EXECUTION, context, payload, mock_trace_manager) + emit(TelemetryCase.NODE_EXECUTION, context, payload, mock_trace_manager) mock_trace_manager.add_trace_task.assert_called_once() class TestGatewayIntegrationMetricRouting: - @pytest.fixture - def gateway(self) -> TelemetryGateway: - return TelemetryGateway() - + @patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=True) def test_metric_case_routes_to_celery_task( self, - gateway: TelemetryGateway, + _mock_ee_enabled: MagicMock, ) -> None: from enterprise.telemetry.contracts import TelemetryEnvelope @@ -118,7 +106,7 @@ class TestGatewayIntegrationMetricRouting: context = {"tenant_id": "tenant-123"} payload = {"app_id": "app-abc", "name": "My App"} - gateway.emit(TelemetryCase.APP_CREATED, context, payload) + emit(TelemetryCase.APP_CREATED, context, payload) mock_delay.assert_called_once() envelope_json = mock_delay.call_args[0][0] @@ -127,46 +115,36 @@ class TestGatewayIntegrationMetricRouting: assert envelope.tenant_id == "tenant-123" assert envelope.payload["app_id"] == "app-abc" - def test_tool_execution_metric_routed( + @pytest.mark.usefixtures("mock_ops_trace_manager") + @patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=True) + def test_tool_execution_trace_routed( self, - gateway: TelemetryGateway, + _mock_ee_enabled: MagicMock, ) -> None: - from enterprise.telemetry.contracts import TelemetryEnvelope + mock_trace_manager = MagicMock() + context = {"tenant_id": "tenant-123", "app_id": "app-123"} + payload = {"tool_name": "test_tool", "tool_inputs": {}, "tool_outputs": "result"} - with patch("tasks.enterprise_telemetry_task.process_enterprise_telemetry.delay") as mock_delay: - context = {"tenant_id": "tenant-123", "app_id": "app-123"} - payload = {"tool_name": "test_tool", "tool_inputs": {}, "tool_outputs": "result"} + emit(TelemetryCase.TOOL_EXECUTION, context, payload, mock_trace_manager) - gateway.emit(TelemetryCase.TOOL_EXECUTION, context, payload) + mock_trace_manager.add_trace_task.assert_called_once() - mock_delay.assert_called_once() - envelope_json = mock_delay.call_args[0][0] - envelope = TelemetryEnvelope.model_validate_json(envelope_json) - assert envelope.case == TelemetryCase.TOOL_EXECUTION - - def test_moderation_check_metric_routed( + @pytest.mark.usefixtures("mock_ops_trace_manager") + @patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=True) + def test_moderation_check_trace_routed( self, - gateway: TelemetryGateway, + _mock_ee_enabled: MagicMock, ) -> None: - from enterprise.telemetry.contracts import TelemetryEnvelope + mock_trace_manager = MagicMock() + context = {"tenant_id": "tenant-123", "app_id": "app-123"} + payload = {"message_id": "msg-123", "moderation_result": {"flagged": False}} - with patch("tasks.enterprise_telemetry_task.process_enterprise_telemetry.delay") as mock_delay: - context = {"tenant_id": "tenant-123", "app_id": "app-123"} - payload = {"message_id": "msg-123", "moderation_result": {"flagged": False}} + emit(TelemetryCase.MODERATION_CHECK, context, payload, mock_trace_manager) - gateway.emit(TelemetryCase.MODERATION_CHECK, context, payload) - - mock_delay.assert_called_once() - envelope_json = mock_delay.call_args[0][0] - envelope = TelemetryEnvelope.model_validate_json(envelope_json) - assert envelope.case == TelemetryCase.MODERATION_CHECK + mock_trace_manager.add_trace_task.assert_called_once() class TestGatewayIntegrationCEEligibility: - @pytest.fixture - def gateway(self) -> TelemetryGateway: - return TelemetryGateway() - @pytest.fixture def mock_trace_manager(self) -> MagicMock: return MagicMock() @@ -174,70 +152,65 @@ class TestGatewayIntegrationCEEligibility: @pytest.mark.usefixtures("mock_ops_trace_manager") def test_workflow_run_is_ce_eligible( self, - gateway: TelemetryGateway, mock_trace_manager: MagicMock, ) -> None: - with patch("enterprise.telemetry.gateway._is_enterprise_telemetry_enabled", return_value=False): + with patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=False): context = {"app_id": "app-123", "user_id": "user-456"} payload = {"workflow_run_id": "run-abc"} - gateway.emit(TelemetryCase.WORKFLOW_RUN, context, payload, mock_trace_manager) + emit(TelemetryCase.WORKFLOW_RUN, context, payload, mock_trace_manager) mock_trace_manager.add_trace_task.assert_called_once() @pytest.mark.usefixtures("mock_ops_trace_manager") def test_message_run_is_ce_eligible( self, - gateway: TelemetryGateway, mock_trace_manager: MagicMock, ) -> None: - with patch("enterprise.telemetry.gateway._is_enterprise_telemetry_enabled", return_value=False): + with patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=False): context = {"app_id": "app-123", "user_id": "user-456"} payload = {"message_id": "msg-abc", "conversation_id": "conv-123"} - gateway.emit(TelemetryCase.MESSAGE_RUN, context, payload, mock_trace_manager) + emit(TelemetryCase.MESSAGE_RUN, context, payload, mock_trace_manager) mock_trace_manager.add_trace_task.assert_called_once() @pytest.mark.usefixtures("mock_ops_trace_manager") def test_node_execution_not_ce_eligible( self, - gateway: TelemetryGateway, mock_trace_manager: MagicMock, ) -> None: - with patch("enterprise.telemetry.gateway._is_enterprise_telemetry_enabled", return_value=False): + with patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=False): context = {"app_id": "app-123", "user_id": "user-456"} payload = {"node_id": "node-abc"} - gateway.emit(TelemetryCase.NODE_EXECUTION, context, payload, mock_trace_manager) + emit(TelemetryCase.NODE_EXECUTION, context, payload, mock_trace_manager) mock_trace_manager.add_trace_task.assert_not_called() @pytest.mark.usefixtures("mock_ops_trace_manager") def test_draft_node_execution_not_ce_eligible( self, - gateway: TelemetryGateway, mock_trace_manager: MagicMock, ) -> None: - with patch("enterprise.telemetry.gateway._is_enterprise_telemetry_enabled", return_value=False): + with patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=False): context = {"app_id": "app-123", "user_id": "user-456"} payload = {"node_execution_data": {}} - gateway.emit(TelemetryCase.DRAFT_NODE_EXECUTION, context, payload, mock_trace_manager) + emit(TelemetryCase.DRAFT_NODE_EXECUTION, context, payload, mock_trace_manager) mock_trace_manager.add_trace_task.assert_not_called() @pytest.mark.usefixtures("mock_ops_trace_manager") def test_prompt_generation_not_ce_eligible( self, - gateway: TelemetryGateway, mock_trace_manager: MagicMock, ) -> None: - with patch("enterprise.telemetry.gateway._is_enterprise_telemetry_enabled", return_value=False): + with patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=False): context = {"app_id": "app-123", "user_id": "user-456", "tenant_id": "tenant-789"} payload = {"operation_type": "generate", "instruction": "test"} - gateway.emit(TelemetryCase.PROMPT_GENERATION, context, payload, mock_trace_manager) + emit(TelemetryCase.PROMPT_GENERATION, context, payload, mock_trace_manager) mock_trace_manager.add_trace_task.assert_not_called() diff --git a/api/tests/unit_tests/enterprise/telemetry/test_contracts.py b/api/tests/unit_tests/enterprise/telemetry/test_contracts.py index ce2162c5f4..7453525bfc 100644 --- a/api/tests/unit_tests/enterprise/telemetry/test_contracts.py +++ b/api/tests/unit_tests/enterprise/telemetry/test_contracts.py @@ -5,8 +5,8 @@ from __future__ import annotations import pytest from pydantic import ValidationError +from core.telemetry.gateway import CASE_ROUTING from enterprise.telemetry.contracts import CaseRoute, SignalType, TelemetryCase, TelemetryEnvelope -from enterprise.telemetry.gateway import CASE_ROUTING class TestTelemetryCase: @@ -87,26 +87,22 @@ class TestTelemetryEnvelope: assert envelope.tenant_id == "tenant-123" assert envelope.event_id == "event-456" assert envelope.payload == {"key": "value"} - assert envelope.payload_fallback is None assert envelope.metadata is None def test_valid_envelope_full(self) -> None: """Verify valid envelope with all fields.""" - metadata = {"source": "api"} - fallback = b"fallback data" + metadata = {"payload_ref": "telemetry/tenant-789/event-012.json"} envelope = TelemetryEnvelope( case=TelemetryCase.MESSAGE_RUN, tenant_id="tenant-789", event_id="event-012", payload={"message": "hello"}, - payload_fallback=fallback, metadata=metadata, ) assert envelope.case == TelemetryCase.MESSAGE_RUN assert envelope.tenant_id == "tenant-789" assert envelope.event_id == "event-012" assert envelope.payload == {"message": "hello"} - assert envelope.payload_fallback == fallback assert envelope.metadata == metadata def test_missing_required_case(self) -> None: @@ -145,41 +141,16 @@ class TestTelemetryEnvelope: event_id="event-456", ) - def test_payload_fallback_within_limit(self) -> None: - """Verify payload_fallback within 64KB limit is accepted.""" - fallback = b"x" * 65536 + def test_metadata_none(self) -> None: + """Verify metadata can be None.""" envelope = TelemetryEnvelope( case=TelemetryCase.WORKFLOW_RUN, tenant_id="tenant-123", event_id="event-456", payload={"key": "value"}, - payload_fallback=fallback, + metadata=None, ) - assert envelope.payload_fallback == fallback - - def test_payload_fallback_exceeds_limit(self) -> None: - """Verify payload_fallback exceeding 64KB is rejected.""" - fallback = b"x" * 65537 - with pytest.raises(ValidationError) as exc_info: - TelemetryEnvelope( - case=TelemetryCase.WORKFLOW_RUN, - tenant_id="tenant-123", - event_id="event-456", - payload={"key": "value"}, - payload_fallback=fallback, - ) - assert "64KB" in str(exc_info.value) - - def test_payload_fallback_none(self) -> None: - """Verify payload_fallback can be None.""" - envelope = TelemetryEnvelope( - case=TelemetryCase.WORKFLOW_RUN, - tenant_id="tenant-123", - event_id="event-456", - payload={"key": "value"}, - payload_fallback=None, - ) - assert envelope.payload_fallback is None + assert envelope.metadata is None class TestCaseRouting: @@ -221,11 +192,6 @@ class TestCaseRouting: TelemetryCase.APP_UPDATED, TelemetryCase.APP_DELETED, TelemetryCase.FEEDBACK_CREATED, - TelemetryCase.TOOL_EXECUTION, - TelemetryCase.MODERATION_CHECK, - TelemetryCase.SUGGESTED_QUESTION, - TelemetryCase.DATASET_RETRIEVAL, - TelemetryCase.GENERATE_NAME, } for case in metric_log_cases: route = CASE_ROUTING[case] @@ -240,17 +206,17 @@ class TestCaseRouting: TelemetryCase.NODE_EXECUTION, TelemetryCase.DRAFT_NODE_EXECUTION, TelemetryCase.PROMPT_GENERATION, + TelemetryCase.TOOL_EXECUTION, + TelemetryCase.MODERATION_CHECK, + TelemetryCase.SUGGESTED_QUESTION, + TelemetryCase.DATASET_RETRIEVAL, + TelemetryCase.GENERATE_NAME, } metric_log_cases = { TelemetryCase.APP_CREATED, TelemetryCase.APP_UPDATED, TelemetryCase.APP_DELETED, TelemetryCase.FEEDBACK_CREATED, - TelemetryCase.TOOL_EXECUTION, - TelemetryCase.MODERATION_CHECK, - TelemetryCase.SUGGESTED_QUESTION, - TelemetryCase.DATASET_RETRIEVAL, - TelemetryCase.GENERATE_NAME, } all_cases = trace_cases | metric_log_cases diff --git a/api/tests/unit_tests/enterprise/telemetry/test_event_handlers.py b/api/tests/unit_tests/enterprise/telemetry/test_event_handlers.py index 13902e8340..ad15c9f096 100644 --- a/api/tests/unit_tests/enterprise/telemetry/test_event_handlers.py +++ b/api/tests/unit_tests/enterprise/telemetry/test_event_handlers.py @@ -7,20 +7,12 @@ from enterprise.telemetry.contracts import TelemetryCase @pytest.fixture -def mock_exporter(): - with patch("extensions.ext_enterprise_telemetry.get_enterprise_exporter") as mock: - exporter = MagicMock() - mock.return_value = exporter - yield exporter - - -@pytest.fixture -def mock_task(): - with patch("tasks.enterprise_telemetry_task.process_enterprise_telemetry") as mock: +def mock_gateway_emit(): + with patch("core.telemetry.gateway.emit") as mock: yield mock -def test_handle_app_created_calls_task(mock_exporter, mock_task): +def test_handle_app_created_calls_task(mock_gateway_emit): sender = MagicMock() sender.id = "app-123" sender.tenant_id = "tenant-456" @@ -28,54 +20,53 @@ def test_handle_app_created_calls_task(mock_exporter, mock_task): event_handlers._handle_app_created(sender) - mock_task.delay.assert_called_once() - call_args = mock_task.delay.call_args[0][0] - assert "app_created" in call_args - assert "tenant-456" in call_args - assert "app-123" in call_args - assert "chat" in call_args + mock_gateway_emit.assert_called_once_with( + case=TelemetryCase.APP_CREATED, + context={"tenant_id": "tenant-456"}, + payload={"app_id": "app-123", "mode": "chat"}, + ) -def test_handle_app_created_no_exporter(mock_task): - with patch("extensions.ext_enterprise_telemetry.get_enterprise_exporter", return_value=None): - sender = MagicMock() - sender.id = "app-123" - sender.tenant_id = "tenant-456" +def test_handle_app_created_no_exporter(mock_gateway_emit): + """Gateway handles exporter availability internally; handler always calls gateway.""" + sender = MagicMock() + sender.id = "app-123" + sender.tenant_id = "tenant-456" - event_handlers._handle_app_created(sender) + event_handlers._handle_app_created(sender) - mock_task.delay.assert_not_called() + mock_gateway_emit.assert_called_once() -def test_handle_app_updated_calls_task(mock_exporter, mock_task): +def test_handle_app_updated_calls_task(mock_gateway_emit): sender = MagicMock() sender.id = "app-123" sender.tenant_id = "tenant-456" event_handlers._handle_app_updated(sender) - mock_task.delay.assert_called_once() - call_args = mock_task.delay.call_args[0][0] - assert "app_updated" in call_args - assert "tenant-456" in call_args - assert "app-123" in call_args + mock_gateway_emit.assert_called_once_with( + case=TelemetryCase.APP_UPDATED, + context={"tenant_id": "tenant-456"}, + payload={"app_id": "app-123"}, + ) -def test_handle_app_deleted_calls_task(mock_exporter, mock_task): +def test_handle_app_deleted_calls_task(mock_gateway_emit): sender = MagicMock() sender.id = "app-123" sender.tenant_id = "tenant-456" event_handlers._handle_app_deleted(sender) - mock_task.delay.assert_called_once() - call_args = mock_task.delay.call_args[0][0] - assert "app_deleted" in call_args - assert "tenant-456" in call_args - assert "app-123" in call_args + mock_gateway_emit.assert_called_once_with( + case=TelemetryCase.APP_DELETED, + context={"tenant_id": "tenant-456"}, + payload={"app_id": "app-123"}, + ) -def test_handle_feedback_created_calls_task(mock_exporter, mock_task): +def test_handle_feedback_created_calls_task(mock_gateway_emit): sender = MagicMock() sender.message_id = "msg-123" sender.app_id = "app-456" @@ -88,34 +79,34 @@ def test_handle_feedback_created_calls_task(mock_exporter, mock_task): event_handlers._handle_feedback_created(sender, tenant_id="tenant-456") - mock_task.delay.assert_called_once() - call_args = mock_task.delay.call_args[0][0] - assert "feedback_created" in call_args - assert "tenant-456" in call_args - assert "msg-123" in call_args - assert "app-456" in call_args - assert "conv-789" in call_args - assert "user-001" in call_args - assert "like" in call_args - assert "api" in call_args - assert "Great response!" in call_args + mock_gateway_emit.assert_called_once_with( + case=TelemetryCase.FEEDBACK_CREATED, + context={"tenant_id": "tenant-456"}, + payload={ + "message_id": "msg-123", + "app_id": "app-456", + "conversation_id": "conv-789", + "from_end_user_id": "user-001", + "from_account_id": None, + "rating": "like", + "from_source": "api", + "content": "Great response!", + }, + ) -def test_handle_feedback_created_no_exporter(mock_task): - with patch("extensions.ext_enterprise_telemetry.get_enterprise_exporter", return_value=None): - sender = MagicMock() - sender.message_id = "msg-123" +def test_handle_feedback_created_no_exporter(mock_gateway_emit): + """Gateway handles exporter availability internally; handler always calls gateway.""" + sender = MagicMock() + sender.message_id = "msg-123" - event_handlers._handle_feedback_created(sender, tenant_id="tenant-456") + event_handlers._handle_feedback_created(sender, tenant_id="tenant-456") - mock_task.delay.assert_not_called() + mock_gateway_emit.assert_called_once() -def test_handlers_create_valid_envelopes(mock_exporter, mock_task): - import json - - from enterprise.telemetry.contracts import TelemetryEnvelope - +def test_handlers_create_valid_envelopes(mock_gateway_emit): + """Verify handlers pass correct TelemetryCase and payload structure.""" sender = MagicMock() sender.id = "app-123" sender.tenant_id = "tenant-456" @@ -123,12 +114,8 @@ def test_handlers_create_valid_envelopes(mock_exporter, mock_task): event_handlers._handle_app_created(sender) - call_args = mock_task.delay.call_args[0][0] - envelope_dict = json.loads(call_args) - envelope = TelemetryEnvelope(**envelope_dict) - - assert envelope.case == TelemetryCase.APP_CREATED - assert envelope.tenant_id == "tenant-456" - assert envelope.event_id - assert envelope.payload["app_id"] == "app-123" - assert envelope.payload["mode"] == "chat" + call_kwargs = mock_gateway_emit.call_args[1] + assert call_kwargs["case"] == TelemetryCase.APP_CREATED + assert call_kwargs["context"]["tenant_id"] == "tenant-456" + assert call_kwargs["payload"]["app_id"] == "app-123" + assert call_kwargs["payload"]["mode"] == "chat" diff --git a/api/tests/unit_tests/enterprise/telemetry/test_exporter.py b/api/tests/unit_tests/enterprise/telemetry/test_exporter.py index 48fdd308f8..2c367b4118 100644 --- a/api/tests/unit_tests/enterprise/telemetry/test_exporter.py +++ b/api/tests/unit_tests/enterprise/telemetry/test_exporter.py @@ -121,8 +121,8 @@ def test_api_key_overrides_conflicting_header( @patch("enterprise.telemetry.exporter.GRPCSpanExporter") @patch("enterprise.telemetry.exporter.GRPCMetricExporter") -def test_api_key_set_uses_secure_grpc(mock_metric_exporter: MagicMock, mock_span_exporter: MagicMock) -> None: - """Test that API key presence enables TLS (insecure=False) for gRPC.""" +def test_https_endpoint_uses_secure_grpc(mock_metric_exporter: MagicMock, mock_span_exporter: MagicMock) -> None: + """Test that https:// endpoint enables TLS (insecure=False) for gRPC.""" mock_config = SimpleNamespace( ENTERPRISE_OTLP_ENDPOINT="https://collector.example.com", ENTERPRISE_OTLP_HEADERS="", @@ -135,7 +135,7 @@ def test_api_key_set_uses_secure_grpc(mock_metric_exporter: MagicMock, mock_span EnterpriseExporter(mock_config) - # Verify insecure=False for both exporters + # Verify insecure=False for both exporters (https:// scheme) assert mock_span_exporter.call_args is not None assert mock_span_exporter.call_args.kwargs["insecure"] is False @@ -145,8 +145,8 @@ def test_api_key_set_uses_secure_grpc(mock_metric_exporter: MagicMock, mock_span @patch("enterprise.telemetry.exporter.GRPCSpanExporter") @patch("enterprise.telemetry.exporter.GRPCMetricExporter") -def test_no_api_key_uses_insecure_grpc(mock_metric_exporter: MagicMock, mock_span_exporter: MagicMock) -> None: - """Test that empty API key uses insecure gRPC (backward compat).""" +def test_http_endpoint_uses_insecure_grpc(mock_metric_exporter: MagicMock, mock_span_exporter: MagicMock) -> None: + """Test that http:// endpoint uses insecure gRPC (insecure=True).""" mock_config = SimpleNamespace( ENTERPRISE_OTLP_ENDPOINT="http://collector.example.com", ENTERPRISE_OTLP_HEADERS="", @@ -159,7 +159,7 @@ def test_no_api_key_uses_insecure_grpc(mock_metric_exporter: MagicMock, mock_spa EnterpriseExporter(mock_config) - # Verify insecure=True for both exporters + # Verify insecure=True for both exporters (http:// scheme) assert mock_span_exporter.call_args is not None assert mock_span_exporter.call_args.kwargs["insecure"] is True @@ -213,3 +213,51 @@ def test_api_key_with_special_chars_preserved(mock_metric_exporter: MagicMock, m headers = mock_span_exporter.call_args.kwargs.get("headers") assert headers is not None assert ("authorization", f"Bearer {special_key}") in headers + + +@patch("enterprise.telemetry.exporter.GRPCSpanExporter") +@patch("enterprise.telemetry.exporter.GRPCMetricExporter") +def test_no_scheme_localhost_uses_insecure(mock_metric_exporter: MagicMock, mock_span_exporter: MagicMock) -> None: + """Test that endpoint without scheme defaults to insecure for localhost.""" + mock_config = SimpleNamespace( + ENTERPRISE_OTLP_ENDPOINT="localhost:4317", + ENTERPRISE_OTLP_HEADERS="", + ENTERPRISE_OTLP_PROTOCOL="grpc", + ENTERPRISE_SERVICE_NAME="dify", + ENTERPRISE_OTEL_SAMPLING_RATE=1.0, + ENTERPRISE_INCLUDE_CONTENT=True, + ENTERPRISE_OTLP_API_KEY="", + ) + + EnterpriseExporter(mock_config) + + # Verify insecure=True for localhost without scheme + assert mock_span_exporter.call_args is not None + assert mock_span_exporter.call_args.kwargs["insecure"] is True + + assert mock_metric_exporter.call_args is not None + assert mock_metric_exporter.call_args.kwargs["insecure"] is True + + +@patch("enterprise.telemetry.exporter.GRPCSpanExporter") +@patch("enterprise.telemetry.exporter.GRPCMetricExporter") +def test_no_scheme_production_uses_insecure(mock_metric_exporter: MagicMock, mock_span_exporter: MagicMock) -> None: + """Test that endpoint without scheme defaults to insecure (not https://).""" + mock_config = SimpleNamespace( + ENTERPRISE_OTLP_ENDPOINT="collector.example.com:4317", + ENTERPRISE_OTLP_HEADERS="", + ENTERPRISE_OTLP_PROTOCOL="grpc", + ENTERPRISE_SERVICE_NAME="dify", + ENTERPRISE_OTEL_SAMPLING_RATE=1.0, + ENTERPRISE_INCLUDE_CONTENT=True, + ENTERPRISE_OTLP_API_KEY="", + ) + + EnterpriseExporter(mock_config) + + # Verify insecure=True for any endpoint without https:// scheme + assert mock_span_exporter.call_args is not None + assert mock_span_exporter.call_args.kwargs["insecure"] is True + + assert mock_metric_exporter.call_args is not None + assert mock_metric_exporter.call_args.kwargs["insecure"] is True diff --git a/api/tests/unit_tests/enterprise/telemetry/test_gateway.py b/api/tests/unit_tests/enterprise/telemetry/test_gateway.py index ff226dd56c..d979dc7336 100644 --- a/api/tests/unit_tests/enterprise/telemetry/test_gateway.py +++ b/api/tests/unit_tests/enterprise/telemetry/test_gateway.py @@ -6,14 +6,13 @@ from unittest.mock import MagicMock, patch import pytest from core.ops.entities.trace_entity import TraceTaskName -from enterprise.telemetry.contracts import SignalType, TelemetryCase, TelemetryEnvelope -from enterprise.telemetry.gateway import ( +from core.telemetry.gateway import ( CASE_ROUTING, CASE_TO_TRACE_TASK, PAYLOAD_SIZE_THRESHOLD_BYTES, - TelemetryGateway, emit, ) +from enterprise.telemetry.contracts import SignalType, TelemetryCase, TelemetryEnvelope class TestCaseRoutingTable: @@ -38,17 +37,20 @@ class TestCaseRoutingTable: TelemetryCase.APP_UPDATED, TelemetryCase.APP_DELETED, TelemetryCase.FEEDBACK_CREATED, + ] + for case in metric_log_cases: + assert CASE_ROUTING[case].signal_type is SignalType.METRIC_LOG, f"{case} should be metric_log" + + def test_ce_eligible_cases(self) -> None: + ce_eligible_cases = [ + TelemetryCase.WORKFLOW_RUN, + TelemetryCase.MESSAGE_RUN, TelemetryCase.TOOL_EXECUTION, TelemetryCase.MODERATION_CHECK, TelemetryCase.SUGGESTED_QUESTION, TelemetryCase.DATASET_RETRIEVAL, TelemetryCase.GENERATE_NAME, ] - for case in metric_log_cases: - assert CASE_ROUTING[case].signal_type is SignalType.METRIC_LOG, f"{case} should be metric_log" - - def test_ce_eligible_cases(self) -> None: - ce_eligible_cases = [TelemetryCase.WORKFLOW_RUN, TelemetryCase.MESSAGE_RUN] for case in ce_eligible_cases: assert CASE_ROUTING[case].ce_eligible is True, f"{case} should be CE eligible" @@ -87,91 +89,80 @@ def mock_ops_trace_manager(): yield mock_module, mock_trace_entity -class TestTelemetryGatewayTraceRouting: - @pytest.fixture - def gateway(self) -> TelemetryGateway: - return TelemetryGateway() - +class TestGatewayTraceRouting: @pytest.fixture def mock_trace_manager(self) -> MagicMock: return MagicMock() - @patch("enterprise.telemetry.gateway._is_enterprise_telemetry_enabled", return_value=True) + @patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=True) def test_trace_case_routes_to_trace_manager( self, _mock_ee_enabled: MagicMock, - gateway: TelemetryGateway, mock_trace_manager: MagicMock, mock_ops_trace_manager: tuple[MagicMock, MagicMock], ) -> None: context = {"app_id": "app-123", "user_id": "user-456", "tenant_id": "tenant-789"} payload = {"workflow_run_id": "run-abc"} - gateway.emit(TelemetryCase.WORKFLOW_RUN, context, payload, mock_trace_manager) + emit(TelemetryCase.WORKFLOW_RUN, context, payload, mock_trace_manager) mock_trace_manager.add_trace_task.assert_called_once() - @patch("enterprise.telemetry.gateway._is_enterprise_telemetry_enabled", return_value=False) + @patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=False) def test_ce_eligible_trace_enqueued_when_ee_disabled( self, _mock_ee_enabled: MagicMock, - gateway: TelemetryGateway, mock_trace_manager: MagicMock, mock_ops_trace_manager: tuple[MagicMock, MagicMock], ) -> None: context = {"app_id": "app-123", "user_id": "user-456"} payload = {"workflow_run_id": "run-abc"} - gateway.emit(TelemetryCase.WORKFLOW_RUN, context, payload, mock_trace_manager) + emit(TelemetryCase.WORKFLOW_RUN, context, payload, mock_trace_manager) mock_trace_manager.add_trace_task.assert_called_once() - @patch("enterprise.telemetry.gateway._is_enterprise_telemetry_enabled", return_value=False) + @patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=False) def test_enterprise_only_trace_dropped_when_ee_disabled( self, _mock_ee_enabled: MagicMock, - gateway: TelemetryGateway, mock_trace_manager: MagicMock, mock_ops_trace_manager: tuple[MagicMock, MagicMock], ) -> None: context = {"app_id": "app-123", "user_id": "user-456"} payload = {"node_id": "node-abc"} - gateway.emit(TelemetryCase.NODE_EXECUTION, context, payload, mock_trace_manager) + emit(TelemetryCase.NODE_EXECUTION, context, payload, mock_trace_manager) mock_trace_manager.add_trace_task.assert_not_called() - @patch("enterprise.telemetry.gateway._is_enterprise_telemetry_enabled", return_value=True) + @patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=True) def test_enterprise_only_trace_enqueued_when_ee_enabled( self, _mock_ee_enabled: MagicMock, - gateway: TelemetryGateway, mock_trace_manager: MagicMock, mock_ops_trace_manager: tuple[MagicMock, MagicMock], ) -> None: context = {"app_id": "app-123", "user_id": "user-456"} payload = {"node_id": "node-abc"} - gateway.emit(TelemetryCase.NODE_EXECUTION, context, payload, mock_trace_manager) + emit(TelemetryCase.NODE_EXECUTION, context, payload, mock_trace_manager) mock_trace_manager.add_trace_task.assert_called_once() -class TestTelemetryGatewayMetricLogRouting: - @pytest.fixture - def gateway(self) -> TelemetryGateway: - return TelemetryGateway() - +class TestGatewayMetricLogRouting: + @patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=True) @patch("tasks.enterprise_telemetry_task.process_enterprise_telemetry.delay") def test_metric_case_routes_to_celery_task( self, mock_delay: MagicMock, - gateway: TelemetryGateway, + _mock_ee_enabled: MagicMock, ) -> None: context = {"tenant_id": "tenant-123"} payload = {"app_id": "app-abc", "name": "My App"} - gateway.emit(TelemetryCase.APP_CREATED, context, payload) + emit(TelemetryCase.APP_CREATED, context, payload) mock_delay.assert_called_once() envelope_json = mock_delay.call_args[0][0] @@ -180,17 +171,18 @@ class TestTelemetryGatewayMetricLogRouting: assert envelope.tenant_id == "tenant-123" assert envelope.payload["app_id"] == "app-abc" + @patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=True) @patch("tasks.enterprise_telemetry_task.process_enterprise_telemetry.delay") def test_envelope_has_unique_event_id( self, mock_delay: MagicMock, - gateway: TelemetryGateway, + _mock_ee_enabled: MagicMock, ) -> None: context = {"tenant_id": "tenant-123"} payload = {"app_id": "app-abc"} - gateway.emit(TelemetryCase.APP_CREATED, context, payload) - gateway.emit(TelemetryCase.APP_CREATED, context, payload) + emit(TelemetryCase.APP_CREATED, context, payload) + emit(TelemetryCase.APP_CREATED, context, payload) assert mock_delay.call_count == 2 envelope1 = TelemetryEnvelope.model_validate_json(mock_delay.call_args_list[0][0][0]) @@ -198,40 +190,38 @@ class TestTelemetryGatewayMetricLogRouting: assert envelope1.event_id != envelope2.event_id -class TestTelemetryGatewayPayloadSizing: - @pytest.fixture - def gateway(self) -> TelemetryGateway: - return TelemetryGateway() - +class TestGatewayPayloadSizing: + @patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=True) @patch("tasks.enterprise_telemetry_task.process_enterprise_telemetry.delay") def test_small_payload_inlined( self, mock_delay: MagicMock, - gateway: TelemetryGateway, + _mock_ee_enabled: MagicMock, ) -> None: context = {"tenant_id": "tenant-123"} payload = {"key": "small_value"} - gateway.emit(TelemetryCase.APP_CREATED, context, payload) + emit(TelemetryCase.APP_CREATED, context, payload) envelope_json = mock_delay.call_args[0][0] envelope = TelemetryEnvelope.model_validate_json(envelope_json) assert envelope.payload == payload assert envelope.metadata is None - @patch("enterprise.telemetry.gateway.storage") + @patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=True) + @patch("core.telemetry.gateway.storage") @patch("tasks.enterprise_telemetry_task.process_enterprise_telemetry.delay") def test_large_payload_stored( self, mock_delay: MagicMock, mock_storage: MagicMock, - gateway: TelemetryGateway, + _mock_ee_enabled: MagicMock, ) -> None: context = {"tenant_id": "tenant-123"} large_value = "x" * (PAYLOAD_SIZE_THRESHOLD_BYTES + 1000) payload = {"key": large_value} - gateway.emit(TelemetryCase.APP_CREATED, context, payload) + emit(TelemetryCase.APP_CREATED, context, payload) mock_storage.save.assert_called_once() storage_key = mock_storage.save.call_args[0][0] @@ -243,20 +233,21 @@ class TestTelemetryGatewayPayloadSizing: assert envelope.metadata is not None assert envelope.metadata["payload_ref"] == storage_key - @patch("enterprise.telemetry.gateway.storage") + @patch("core.telemetry.gateway.is_enterprise_telemetry_enabled", return_value=True) + @patch("core.telemetry.gateway.storage") @patch("tasks.enterprise_telemetry_task.process_enterprise_telemetry.delay") def test_large_payload_fallback_on_storage_error( self, mock_delay: MagicMock, mock_storage: MagicMock, - gateway: TelemetryGateway, + _mock_ee_enabled: MagicMock, ) -> None: mock_storage.save.side_effect = Exception("Storage failure") context = {"tenant_id": "tenant-123"} large_value = "x" * (PAYLOAD_SIZE_THRESHOLD_BYTES + 1000) payload = {"key": large_value} - gateway.emit(TelemetryCase.APP_CREATED, context, payload) + emit(TelemetryCase.APP_CREATED, context, payload) envelope_json = mock_delay.call_args[0][0] envelope = TelemetryEnvelope.model_validate_json(envelope_json) @@ -264,26 +255,6 @@ class TestTelemetryGatewayPayloadSizing: assert envelope.metadata is None -class TestModuleLevelFunctions: - @patch("extensions.ext_enterprise_telemetry.get_gateway") - @patch("enterprise.telemetry.gateway._is_enterprise_telemetry_enabled", return_value=True) - def test_emit_function_uses_gateway( - self, - _mock_ee_enabled: MagicMock, - mock_get_gateway: MagicMock, - mock_ops_trace_manager: tuple[MagicMock, MagicMock], - ) -> None: - mock_gateway = TelemetryGateway() - mock_get_gateway.return_value = mock_gateway - mock_trace_manager = MagicMock() - context = {"app_id": "app-123", "user_id": "user-456"} - payload = {"workflow_run_id": "run-abc"} - - with patch.object(mock_gateway, "emit") as mock_emit: - emit(TelemetryCase.WORKFLOW_RUN, context, payload, mock_trace_manager) - mock_emit.assert_called_once_with(TelemetryCase.WORKFLOW_RUN, context, payload, mock_trace_manager) - - class TestTraceTaskNameMapping: def test_workflow_run_mapping(self) -> None: assert CASE_TO_TRACE_TASK[TelemetryCase.WORKFLOW_RUN] is TraceTaskName.WORKFLOW_TRACE diff --git a/api/tests/unit_tests/enterprise/telemetry/test_metric_handler.py b/api/tests/unit_tests/enterprise/telemetry/test_metric_handler.py index a858c8e95a..19822fd69f 100644 --- a/api/tests/unit_tests/enterprise/telemetry/test_metric_handler.py +++ b/api/tests/unit_tests/enterprise/telemetry/test_metric_handler.py @@ -1,5 +1,6 @@ """Unit tests for EnterpriseMetricHandler.""" +import json from unittest.mock import MagicMock, patch import pytest @@ -238,31 +239,60 @@ def test_rehydration_uses_payload(sample_envelope): assert payload == {"app_id": "app-123", "name": "Test App"} -def test_rehydration_fallback(): - import pickle - - fallback_data = {"fallback": "data"} +def test_rehydration_from_storage(): + """Verify _rehydrate loads payload from object storage via payload_ref.""" + stored_data = {"app_id": "app-stored", "mode": "workflow"} envelope = TelemetryEnvelope( case=TelemetryCase.APP_CREATED, tenant_id="test-tenant", event_id="test-event-fb", payload={}, - payload_fallback=pickle.dumps(fallback_data), + metadata={"payload_ref": "telemetry/test-tenant/test-event-fb.json"}, ) handler = EnterpriseMetricHandler() - payload = handler._rehydrate(envelope) + with patch("enterprise.telemetry.metric_handler.storage") as mock_storage: + mock_storage.load.return_value = json.dumps(stored_data).encode("utf-8") + payload = handler._rehydrate(envelope) - assert payload == fallback_data + assert payload == stored_data + mock_storage.load.assert_called_once_with("telemetry/test-tenant/test-event-fb.json") -def test_rehydration_emits_degraded_event_on_failure(): +def test_rehydration_storage_failure_emits_degraded_event(): + """Verify _rehydrate emits degraded event when storage load fails.""" envelope = TelemetryEnvelope( case=TelemetryCase.APP_CREATED, tenant_id="test-tenant", event_id="test-event-fail", payload={}, - payload_fallback=None, + metadata={"payload_ref": "telemetry/test-tenant/test-event-fail.json"}, + ) + + handler = EnterpriseMetricHandler() + with ( + patch("enterprise.telemetry.metric_handler.storage") as mock_storage, + patch("enterprise.telemetry.telemetry_log.emit_metric_only_event") as mock_emit, + ): + mock_storage.load.side_effect = Exception("Storage unavailable") + payload = handler._rehydrate(envelope) + + from enterprise.telemetry.entities import EnterpriseTelemetryEvent + + assert payload == {} + mock_emit.assert_called_once() + call_args = mock_emit.call_args + assert call_args[1]["event_name"] == EnterpriseTelemetryEvent.REHYDRATION_FAILED + assert call_args[1]["attributes"]["rehydration_failed"] is True + + +def test_rehydration_emits_degraded_event_on_empty_payload(): + """Verify _rehydrate emits degraded event when payload is empty and no ref exists.""" + envelope = TelemetryEnvelope( + case=TelemetryCase.APP_CREATED, + tenant_id="test-tenant", + event_id="test-event-empty", + payload={}, ) handler = EnterpriseMetricHandler() @@ -304,6 +334,7 @@ def test_on_app_created_emits_correct_event(mock_redis): attributes={ "dify.app.id": "app-789", "dify.tenant_id": "tenant-123", + "dify.event.id": "event-456", "dify.app.mode": "chat", }, tenant_id="tenant-123", @@ -345,6 +376,7 @@ def test_on_app_updated_emits_correct_event(mock_redis): attributes={ "dify.app.id": "app-789", "dify.tenant_id": "tenant-123", + "dify.event.id": "event-456", }, tenant_id="tenant-123", ) @@ -384,6 +416,7 @@ def test_on_app_deleted_emits_correct_event(mock_redis): attributes={ "dify.app.id": "app-789", "dify.tenant_id": "tenant-123", + "dify.event.id": "event-456", }, tenant_id="tenant-123", ) diff --git a/api/uv.lock b/api/uv.lock index a3b5433952..990150ed73 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1593,14 +1593,14 @@ requires-dist = [ { name = "flask-sqlalchemy", specifier = "~=3.1.1" }, { name = "gevent", specifier = "~=25.9.1" }, { name = "gmpy2", specifier = "~=2.2.1" }, - { name = "google-api-core", specifier = "==2.18.0" }, + { name = "google-api-core", specifier = ">=2.19.1" }, { name = "google-api-python-client", specifier = "==2.90.0" }, - { name = "google-auth", specifier = "==2.29.0" }, + { name = "google-auth", specifier = ">=2.47.0" }, { name = "google-auth-httplib2", specifier = "==0.2.0" }, - { name = "google-cloud-aiplatform", specifier = "==1.49.0" }, - { name = "googleapis-common-protos", specifier = "==1.63.0" }, + { name = "google-cloud-aiplatform", specifier = ">=1.123.0" }, + { name = "googleapis-common-protos", specifier = ">=1.65.0" }, { name = "gunicorn", specifier = "~=23.0.0" }, - { name = "httpx", extras = ["socks"], specifier = "~=0.27.0" }, + { name = "httpx", extras = ["socks"], specifier = "~=0.28.0" }, { name = "httpx-sse", specifier = "~=0.4.0" }, { name = "jieba", specifier = "==0.42.1" }, { name = "json-repair", specifier = ">=0.55.1" }, @@ -1612,23 +1612,23 @@ requires-dist = [ { name = "mlflow-skinny", specifier = ">=3.0.0" }, { name = "numpy", specifier = "~=1.26.4" }, { name = "openpyxl", specifier = "~=3.1.5" }, - { name = "opentelemetry-api", specifier = "==1.27.0" }, - { name = "opentelemetry-distro", specifier = "==0.48b0" }, - { name = "opentelemetry-exporter-otlp", specifier = "==1.27.0" }, - { name = "opentelemetry-exporter-otlp-proto-common", specifier = "==1.27.0" }, - { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = "==1.27.0" }, - { name = "opentelemetry-exporter-otlp-proto-http", specifier = "==1.27.0" }, - { name = "opentelemetry-instrumentation", specifier = "==0.48b0" }, - { name = "opentelemetry-instrumentation-celery", specifier = "==0.48b0" }, - { name = "opentelemetry-instrumentation-flask", specifier = "==0.48b0" }, - { name = "opentelemetry-instrumentation-httpx", specifier = "==0.48b0" }, - { name = "opentelemetry-instrumentation-redis", specifier = "==0.48b0" }, - { name = "opentelemetry-instrumentation-sqlalchemy", specifier = "==0.48b0" }, - { name = "opentelemetry-propagator-b3", specifier = "==1.27.0" }, - { name = "opentelemetry-proto", specifier = "==1.27.0" }, - { name = "opentelemetry-sdk", specifier = "==1.27.0" }, - { name = "opentelemetry-semantic-conventions", specifier = "==0.48b0" }, - { name = "opentelemetry-util-http", specifier = "==0.48b0" }, + { name = "opentelemetry-api", specifier = "==1.28.0" }, + { name = "opentelemetry-distro", specifier = "==0.49b0" }, + { name = "opentelemetry-exporter-otlp", specifier = "==1.28.0" }, + { name = "opentelemetry-exporter-otlp-proto-common", specifier = "==1.28.0" }, + { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = "==1.28.0" }, + { name = "opentelemetry-exporter-otlp-proto-http", specifier = "==1.28.0" }, + { name = "opentelemetry-instrumentation", specifier = "==0.49b0" }, + { name = "opentelemetry-instrumentation-celery", specifier = "==0.49b0" }, + { name = "opentelemetry-instrumentation-flask", specifier = "==0.49b0" }, + { name = "opentelemetry-instrumentation-httpx", specifier = "==0.49b0" }, + { name = "opentelemetry-instrumentation-redis", specifier = "==0.49b0" }, + { name = "opentelemetry-instrumentation-sqlalchemy", specifier = "==0.49b0" }, + { name = "opentelemetry-propagator-b3", specifier = "==1.28.0" }, + { name = "opentelemetry-proto", specifier = "==1.28.0" }, + { name = "opentelemetry-sdk", specifier = "==1.28.0" }, + { name = "opentelemetry-semantic-conventions", specifier = "==0.49b0" }, + { name = "opentelemetry-util-http", specifier = "==0.49b0" }, { name = "opik", specifier = "~=1.8.72" }, { name = "packaging", specifier = "~=23.2" }, { name = "pandas", extras = ["excel", "output-formatting", "performance"], specifier = "~=2.2.2" }, @@ -2284,7 +2284,7 @@ wheels = [ [[package]] name = "google-api-core" -version = "2.18.0" +version = "2.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, @@ -2293,9 +2293,9 @@ dependencies = [ { name = "protobuf" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/8f/ecd68579bd2bf5e9321df60dcdee6e575adf77fedacb1d8378760b2b16b6/google-api-core-2.18.0.tar.gz", hash = "sha256:62d97417bfc674d6cef251e5c4d639a9655e00c45528c4364fbfebb478ce72a9", size = 148047, upload-time = "2024-03-21T20:16:56.269Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/98/586ec94553b569080caef635f98a3723db36a38eac0e3d7eb3ea9d2e4b9a/google_api_core-2.30.0.tar.gz", hash = "sha256:02edfa9fab31e17fc0befb5f161b3bf93c9096d99aed584625f38065c511ad9b", size = 176959, upload-time = "2026-02-18T20:28:11.926Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/75/59a3ad90d9b4ff5b3e0537611dbe885aeb96124521c9d35aa079f1e0f2c9/google_api_core-2.18.0-py3-none-any.whl", hash = "sha256:5a63aa102e0049abe85b5b88cb9409234c1f70afcda21ce1e40b285b9629c1d6", size = 138293, upload-time = "2024-03-21T20:16:53.645Z" }, + { url = "https://files.pythonhosted.org/packages/45/27/09c33d67f7e0dcf06d7ac17d196594e66989299374bfb0d4331d1038e76b/google_api_core-2.30.0-py3-none-any.whl", hash = "sha256:80be49ee937ff9aba0fd79a6eddfde35fe658b9953ab9b79c57dd7061afa8df5", size = 173288, upload-time = "2026-02-18T20:28:10.367Z" }, ] [package.optional-dependencies] @@ -2322,16 +2322,21 @@ wheels = [ [[package]] name = "google-auth" -version = "2.29.0" +version = "2.48.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cachetools" }, + { name = "cryptography" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/b2/f14129111cfd61793609643a07ecb03651a71dd65c6974f63b0310ff4b45/google-auth-2.29.0.tar.gz", hash = "sha256:672dff332d073227550ffc7457868ac4218d6c500b155fe6cc17d2b13602c360", size = 244326, upload-time = "2024-03-20T17:24:27.72Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/8d/ddbcf81ec751d8ee5fd18ac11ff38a0e110f39dfbf105e6d9db69d556dd0/google_auth-2.29.0-py2.py3-none-any.whl", hash = "sha256:d452ad095688cd52bae0ad6fafe027f6a6d6f560e810fec20914e17a09526415", size = 189186, upload-time = "2024-03-20T17:24:24.292Z" }, + { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests" }, ] [[package]] @@ -2349,7 +2354,7 @@ wheels = [ [[package]] name = "google-cloud-aiplatform" -version = "1.49.0" +version = "1.139.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docstring-parser" }, @@ -2358,15 +2363,16 @@ dependencies = [ { name = "google-cloud-bigquery" }, { name = "google-cloud-resource-manager" }, { name = "google-cloud-storage" }, + { name = "google-genai" }, { name = "packaging" }, { name = "proto-plus" }, { name = "protobuf" }, { name = "pydantic" }, - { name = "shapely" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/47/21/5930a1420f82bec246ae09e1b7cc8458544f3befe669193b33a7b5c0691c/google-cloud-aiplatform-1.49.0.tar.gz", hash = "sha256:e6e6d01079bb5def49e4be4db4d12b13c624b5c661079c869c13c855e5807429", size = 5766450, upload-time = "2024-04-29T17:25:31.646Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/40/6767bd4d694354fd55842990da66f7b6ccfdce283d10f65d4a82d9a8e8df/google_cloud_aiplatform-1.139.0.tar.gz", hash = "sha256:cfaa95375bfb79a97b8c949c3ec1600505a4a9c08ca2b01c36ed659a5e05e37c", size = 9964138, upload-time = "2026-02-25T00:51:06.976Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/6a/7d9e1c03c814e760361fe8b0ffd373ead4124ace66ed33bb16d526ae1ecf/google_cloud_aiplatform-1.49.0-py2.py3-none-any.whl", hash = "sha256:8072d9e0c18d8942c704233d1a93b8d6312fc7b278786a283247950e28ae98df", size = 4914049, upload-time = "2024-04-29T17:25:27.625Z" }, + { url = "https://files.pythonhosted.org/packages/1d/20/a8a77dfdbf2a8169a3cce2d4e9cfbbfc168454ddd435891e59908ea8bf33/google_cloud_aiplatform-1.139.0-py2.py3-none-any.whl", hash = "sha256:3190b255cf510bce9e4b1adc8162ab0b3f9eca48801657d7af058d8e1d5ad9d0", size = 8209776, upload-time = "2026-02-25T00:51:03.526Z" }, ] [[package]] @@ -2454,6 +2460,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/3c/2a19a60a473de48717b4efb19398c3f914795b64a96cf3fbe82588044f78/google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82", size = 28048, upload-time = "2025-03-26T14:41:46.696Z" }, ] +[[package]] +name = "google-genai" +version = "1.65.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/f9/cc1191c2540d6a4e24609a586c4ed45d2db57cfef47931c139ee70e5874a/google_genai-1.65.0.tar.gz", hash = "sha256:d470eb600af802d58a79c7f13342d9ea0d05d965007cae8f76c7adff3d7a4750", size = 497206, upload-time = "2026-02-26T00:20:33.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/3c/3fea4e7c91357c71782d7dcaad7a2577d636c90317e003386893c25bc62c/google_genai-1.65.0-py3-none-any.whl", hash = "sha256:68c025205856919bc03edb0155c11b4b833810b7ce17ad4b7a9eeba5158f6c44", size = 724429, upload-time = "2026-02-26T00:20:32.186Z" }, +] + [[package]] name = "google-resumable-media" version = "2.8.0" @@ -2468,14 +2495,14 @@ wheels = [ [[package]] name = "googleapis-common-protos" -version = "1.63.0" +version = "1.72.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d2/dc/291cebf3c73e108ef8210f19cb83d671691354f4f7dd956445560d778715/googleapis-common-protos-1.63.0.tar.gz", hash = "sha256:17ad01b11d5f1d0171c06d3ba5c04c54474e883b66b949722b4938ee2694ef4e", size = 121646, upload-time = "2024-03-11T12:33:15.765Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/a6/12a0c976140511d8bc8a16ad15793b2aef29ac927baa0786ccb7ddbb6e1c/googleapis_common_protos-1.63.0-py2.py3-none-any.whl", hash = "sha256:ae45f75702f7c08b541f750854a678bd8f534a1a6bace6afe975f1d0a82d6632", size = 229141, upload-time = "2024-03-11T12:33:14.052Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, ] [package.optional-dependencies] @@ -2665,31 +2692,35 @@ wheels = [ [[package]] name = "grpcio-tools" -version = "1.62.3" +version = "1.71.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "grpcio" }, { name = "protobuf" }, { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/54/fa/b69bd8040eafc09b88bb0ec0fea59e8aacd1a801e688af087cead213b0d0/grpcio-tools-1.62.3.tar.gz", hash = "sha256:7c7136015c3d62c3eef493efabaf9e3380e3e66d24ee8e94c01cb71377f57833", size = 4538520, upload-time = "2024-08-06T00:37:11.035Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/9a/edfefb47f11ef6b0f39eea4d8f022c5bb05ac1d14fcc7058e84a51305b73/grpcio_tools-1.71.2.tar.gz", hash = "sha256:b5304d65c7569b21270b568e404a5a843cf027c66552a6a0978b23f137679c09", size = 5330655, upload-time = "2025-06-28T04:22:00.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/52/2dfe0a46b63f5ebcd976570aa5fc62f793d5a8b169e211c6a5aede72b7ae/grpcio_tools-1.62.3-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:703f46e0012af83a36082b5f30341113474ed0d91e36640da713355cd0ea5d23", size = 5147623, upload-time = "2024-08-06T00:30:54.894Z" }, - { url = "https://files.pythonhosted.org/packages/f0/2e/29fdc6c034e058482e054b4a3c2432f84ff2e2765c1342d4f0aa8a5c5b9a/grpcio_tools-1.62.3-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:7cc83023acd8bc72cf74c2edbe85b52098501d5b74d8377bfa06f3e929803492", size = 2719538, upload-time = "2024-08-06T00:30:57.928Z" }, - { url = "https://files.pythonhosted.org/packages/f9/60/abe5deba32d9ec2c76cdf1a2f34e404c50787074a2fee6169568986273f1/grpcio_tools-1.62.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ff7d58a45b75df67d25f8f144936a3e44aabd91afec833ee06826bd02b7fbe7", size = 3070964, upload-time = "2024-08-06T00:31:00.267Z" }, - { url = "https://files.pythonhosted.org/packages/bc/ad/e2b066684c75f8d9a48508cde080a3a36618064b9cadac16d019ca511444/grpcio_tools-1.62.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f2483ea232bd72d98a6dc6d7aefd97e5bc80b15cd909b9e356d6f3e326b6e43", size = 2805003, upload-time = "2024-08-06T00:31:02.565Z" }, - { url = "https://files.pythonhosted.org/packages/9c/3f/59bf7af786eae3f9d24ee05ce75318b87f541d0950190ecb5ffb776a1a58/grpcio_tools-1.62.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:962c84b4da0f3b14b3cdb10bc3837ebc5f136b67d919aea8d7bb3fd3df39528a", size = 3685154, upload-time = "2024-08-06T00:31:05.339Z" }, - { url = "https://files.pythonhosted.org/packages/f1/79/4dd62478b91e27084c67b35a2316ce8a967bd8b6cb8d6ed6c86c3a0df7cb/grpcio_tools-1.62.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8ad0473af5544f89fc5a1ece8676dd03bdf160fb3230f967e05d0f4bf89620e3", size = 3297942, upload-time = "2024-08-06T00:31:08.456Z" }, - { url = "https://files.pythonhosted.org/packages/b8/cb/86449ecc58bea056b52c0b891f26977afc8c4464d88c738f9648da941a75/grpcio_tools-1.62.3-cp311-cp311-win32.whl", hash = "sha256:db3bc9fa39afc5e4e2767da4459df82b095ef0cab2f257707be06c44a1c2c3e5", size = 910231, upload-time = "2024-08-06T00:31:11.464Z" }, - { url = "https://files.pythonhosted.org/packages/45/a4/9736215e3945c30ab6843280b0c6e1bff502910156ea2414cd77fbf1738c/grpcio_tools-1.62.3-cp311-cp311-win_amd64.whl", hash = "sha256:e0898d412a434e768a0c7e365acabe13ff1558b767e400936e26b5b6ed1ee51f", size = 1052496, upload-time = "2024-08-06T00:31:13.665Z" }, - { url = "https://files.pythonhosted.org/packages/2a/a5/d6887eba415ce318ae5005e8dfac3fa74892400b54b6d37b79e8b4f14f5e/grpcio_tools-1.62.3-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:d102b9b21c4e1e40af9a2ab3c6d41afba6bd29c0aa50ca013bf85c99cdc44ac5", size = 5147690, upload-time = "2024-08-06T00:31:16.436Z" }, - { url = "https://files.pythonhosted.org/packages/8a/7c/3cde447a045e83ceb4b570af8afe67ffc86896a2fe7f59594dc8e5d0a645/grpcio_tools-1.62.3-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:0a52cc9444df978438b8d2332c0ca99000521895229934a59f94f37ed896b133", size = 2720538, upload-time = "2024-08-06T00:31:18.905Z" }, - { url = "https://files.pythonhosted.org/packages/88/07/f83f2750d44ac4f06c07c37395b9c1383ef5c994745f73c6bfaf767f0944/grpcio_tools-1.62.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141d028bf5762d4a97f981c501da873589df3f7e02f4c1260e1921e565b376fa", size = 3071571, upload-time = "2024-08-06T00:31:21.684Z" }, - { url = "https://files.pythonhosted.org/packages/37/74/40175897deb61e54aca716bc2e8919155b48f33aafec8043dda9592d8768/grpcio_tools-1.62.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47a5c093ab256dec5714a7a345f8cc89315cb57c298b276fa244f37a0ba507f0", size = 2806207, upload-time = "2024-08-06T00:31:24.208Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ee/d8de915105a217cbcb9084d684abdc032030dcd887277f2ef167372287fe/grpcio_tools-1.62.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f6831fdec2b853c9daa3358535c55eed3694325889aa714070528cf8f92d7d6d", size = 3685815, upload-time = "2024-08-06T00:31:26.917Z" }, - { url = "https://files.pythonhosted.org/packages/fd/d9/4360a6c12be3d7521b0b8c39e5d3801d622fbb81cc2721dbd3eee31e28c8/grpcio_tools-1.62.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e02d7c1a02e3814c94ba0cfe43d93e872c758bd8fd5c2797f894d0c49b4a1dfc", size = 3298378, upload-time = "2024-08-06T00:31:30.401Z" }, - { url = "https://files.pythonhosted.org/packages/29/3b/7cdf4a9e5a3e0a35a528b48b111355cd14da601413a4f887aa99b6da468f/grpcio_tools-1.62.3-cp312-cp312-win32.whl", hash = "sha256:b881fd9505a84457e9f7e99362eeedd86497b659030cf57c6f0070df6d9c2b9b", size = 910416, upload-time = "2024-08-06T00:31:33.118Z" }, - { url = "https://files.pythonhosted.org/packages/6c/66/dd3ec249e44c1cc15e902e783747819ed41ead1336fcba72bf841f72c6e9/grpcio_tools-1.62.3-cp312-cp312-win_amd64.whl", hash = "sha256:11c625eebefd1fd40a228fc8bae385e448c7e32a6ae134e43cf13bbc23f902b7", size = 1052856, upload-time = "2024-08-06T00:31:36.519Z" }, + { url = "https://files.pythonhosted.org/packages/17/e4/0568d38b8da6237ea8ea15abb960fb7ab83eb7bb51e0ea5926dab3d865b1/grpcio_tools-1.71.2-cp311-cp311-linux_armv7l.whl", hash = "sha256:0acb8151ea866be5b35233877fbee6445c36644c0aa77e230c9d1b46bf34b18b", size = 2385557, upload-time = "2025-06-28T04:20:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/76/fb/700d46f72b0f636cf0e625f3c18a4f74543ff127471377e49a071f64f1e7/grpcio_tools-1.71.2-cp311-cp311-macosx_10_14_universal2.whl", hash = "sha256:b28f8606f4123edb4e6da281547465d6e449e89f0c943c376d1732dc65e6d8b3", size = 5447590, upload-time = "2025-06-28T04:20:55.836Z" }, + { url = "https://files.pythonhosted.org/packages/12/69/d9bb2aec3de305162b23c5c884b9f79b1a195d42b1e6dabcc084cc9d0804/grpcio_tools-1.71.2-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:cbae6f849ad2d1f5e26cd55448b9828e678cb947fa32c8729d01998238266a6a", size = 2348495, upload-time = "2025-06-28T04:20:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/d5/83/f840aba1690461b65330efbca96170893ee02fae66651bcc75f28b33a46c/grpcio_tools-1.71.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4d1027615cfb1e9b1f31f2f384251c847d68c2f3e025697e5f5c72e26ed1316", size = 2742333, upload-time = "2025-06-28T04:20:59.051Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/c02cd9b37de26045190ba665ee6ab8597d47f033d098968f812d253bbf8c/grpcio_tools-1.71.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bac95662dc69338edb9eb727cc3dd92342131b84b12b3e8ec6abe973d4cbf1b", size = 2473490, upload-time = "2025-06-28T04:21:00.614Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c7/375718ae091c8f5776828ce97bdcb014ca26244296f8b7f70af1a803ed2f/grpcio_tools-1.71.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c50250c7248055040f89eb29ecad39d3a260a4b6d3696af1575945f7a8d5dcdc", size = 2850333, upload-time = "2025-06-28T04:21:01.95Z" }, + { url = "https://files.pythonhosted.org/packages/19/37/efc69345bd92a73b2bc80f4f9e53d42dfdc234b2491ae58c87da20ca0ea5/grpcio_tools-1.71.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6ab1ad955e69027ef12ace4d700c5fc36341bdc2f420e87881e9d6d02af3d7b8", size = 3300748, upload-time = "2025-06-28T04:21:03.451Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1f/15f787eb25ae42086f55ed3e4260e85f385921c788debf0f7583b34446e3/grpcio_tools-1.71.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dd75dde575781262b6b96cc6d0b2ac6002b2f50882bf5e06713f1bf364ee6e09", size = 2913178, upload-time = "2025-06-28T04:21:04.879Z" }, + { url = "https://files.pythonhosted.org/packages/12/aa/69cb3a9dff7d143a05e4021c3c9b5cde07aacb8eb1c892b7c5b9fb4973e3/grpcio_tools-1.71.2-cp311-cp311-win32.whl", hash = "sha256:9a3cb244d2bfe0d187f858c5408d17cb0e76ca60ec9a274c8fd94cc81457c7fc", size = 946256, upload-time = "2025-06-28T04:21:06.518Z" }, + { url = "https://files.pythonhosted.org/packages/1e/df/fb951c5c87eadb507a832243942e56e67d50d7667b0e5324616ffd51b845/grpcio_tools-1.71.2-cp311-cp311-win_amd64.whl", hash = "sha256:00eb909997fd359a39b789342b476cbe291f4dd9c01ae9887a474f35972a257e", size = 1117661, upload-time = "2025-06-28T04:21:08.18Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d3/3ed30a9c5b2424627b4b8411e2cd6a1a3f997d3812dbc6a8630a78bcfe26/grpcio_tools-1.71.2-cp312-cp312-linux_armv7l.whl", hash = "sha256:bfc0b5d289e383bc7d317f0e64c9dfb59dc4bef078ecd23afa1a816358fb1473", size = 2385479, upload-time = "2025-06-28T04:21:10.413Z" }, + { url = "https://files.pythonhosted.org/packages/54/61/e0b7295456c7e21ef777eae60403c06835160c8d0e1e58ebfc7d024c51d3/grpcio_tools-1.71.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b4669827716355fa913b1376b1b985855d5cfdb63443f8d18faf210180199006", size = 5431521, upload-time = "2025-06-28T04:21:12.261Z" }, + { url = "https://files.pythonhosted.org/packages/75/d7/7bcad6bcc5f5b7fab53e6bce5db87041f38ef3e740b1ec2d8c49534fa286/grpcio_tools-1.71.2-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:d4071f9b44564e3f75cdf0f05b10b3e8c7ea0ca5220acbf4dc50b148552eef2f", size = 2350289, upload-time = "2025-06-28T04:21:13.625Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8a/e4c1c4cb8c9ff7f50b7b2bba94abe8d1e98ea05f52a5db476e7f1c1a3c70/grpcio_tools-1.71.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a28eda8137d587eb30081384c256f5e5de7feda34776f89848b846da64e4be35", size = 2743321, upload-time = "2025-06-28T04:21:15.007Z" }, + { url = "https://files.pythonhosted.org/packages/fd/aa/95bc77fda5c2d56fb4a318c1b22bdba8914d5d84602525c99047114de531/grpcio_tools-1.71.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b19c083198f5eb15cc69c0a2f2c415540cbc636bfe76cea268e5894f34023b40", size = 2474005, upload-time = "2025-06-28T04:21:16.443Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ff/ca11f930fe1daa799ee0ce1ac9630d58a3a3deed3dd2f465edb9a32f299d/grpcio_tools-1.71.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:784c284acda0d925052be19053d35afbf78300f4d025836d424cf632404f676a", size = 2851559, upload-time = "2025-06-28T04:21:18.139Z" }, + { url = "https://files.pythonhosted.org/packages/64/10/c6fc97914c7e19c9bb061722e55052fa3f575165da9f6510e2038d6e8643/grpcio_tools-1.71.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:381e684d29a5d052194e095546eef067201f5af30fd99b07b5d94766f44bf1ae", size = 3300622, upload-time = "2025-06-28T04:21:20.291Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d6/965f36cfc367c276799b730d5dd1311b90a54a33726e561393b808339b04/grpcio_tools-1.71.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3e4b4801fabd0427fc61d50d09588a01b1cfab0ec5e8a5f5d515fbdd0891fd11", size = 2913863, upload-time = "2025-06-28T04:21:22.196Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f0/c05d5c3d0c1d79ac87df964e9d36f1e3a77b60d948af65bec35d3e5c75a3/grpcio_tools-1.71.2-cp312-cp312-win32.whl", hash = "sha256:84ad86332c44572305138eafa4cc30040c9a5e81826993eae8227863b700b490", size = 945744, upload-time = "2025-06-28T04:21:23.463Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e9/c84c1078f0b7af7d8a40f5214a9bdd8d2a567ad6c09975e6e2613a08d29d/grpcio_tools-1.71.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e1108d37eecc73b1c4a27350a6ed921b5dda25091700c1da17cfe30761cd462", size = 1117695, upload-time = "2025-06-28T04:21:25.22Z" }, ] [[package]] @@ -2846,18 +2877,17 @@ wheels = [ [[package]] name = "httpx" -version = "0.27.2" +version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, - { name = "sniffio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189, upload-time = "2024-08-27T12:54:01.334Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395, upload-time = "2024-08-27T12:53:59.653Z" }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] [package.optional-dependencies] @@ -3941,59 +3971,59 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.27.0" +version = "1.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecated" }, { name = "importlib-metadata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/83/93114b6de85a98963aec218a51509a52ed3f8de918fe91eb0f7299805c3f/opentelemetry_api-1.27.0.tar.gz", hash = "sha256:ed673583eaa5f81b5ce5e86ef7cdaf622f88ef65f0b9aab40b843dcae5bef342", size = 62693, upload-time = "2024-08-28T21:35:31.445Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/36/260eaea0f74fdd0c0d8f22ed3a3031109ea1c85531f94f4fde266c29e29a/opentelemetry_api-1.28.0.tar.gz", hash = "sha256:578610bcb8aa5cdcb11169d136cc752958548fb6ccffb0969c1036b0ee9e5353", size = 62803, upload-time = "2024-11-05T19:14:45.497Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/1f/737dcdbc9fea2fa96c1b392ae47275165a7c641663fbb08a8d252968eed2/opentelemetry_api-1.27.0-py3-none-any.whl", hash = "sha256:953d5871815e7c30c81b56d910c707588000fff7a3ca1c73e6531911d53065e7", size = 63970, upload-time = "2024-08-28T21:35:00.598Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/3b25d8b856791c04d8a62b1257b5fc09dc41a057800db06885af8ddcdce1/opentelemetry_api-1.28.0-py3-none-any.whl", hash = "sha256:8457cd2c59ea1bd0988560f021656cecd254ad7ef6be4ba09dbefeca2409ce52", size = 64314, upload-time = "2024-11-05T19:14:21.659Z" }, ] [[package]] name = "opentelemetry-distro" -version = "0.48b0" +version = "0.49b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-sdk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/09/423e17c439ed24c45110affe84aad886a536b7871a42637d2ad14a179b47/opentelemetry_distro-0.48b0.tar.gz", hash = "sha256:5cb15915780ac4972583286a56683d43bd4ca95371d72f5f3f179c8b0b2ddc91", size = 2556, upload-time = "2024-08-28T21:27:40.455Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/75/7cb7c33899e66bb366d40a889111a78c22df0951038b6699f1663e715a9f/opentelemetry_distro-0.49b0.tar.gz", hash = "sha256:1bafa274f9e83baa0d2a5d47ed02caffcf9bcca60107b389b145400d82b07513", size = 2560, upload-time = "2024-11-05T19:21:39.379Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/cf/fa9a5fe954f1942e03b319ae0e319ebc93d9f984b548bcd9b3f232a1434d/opentelemetry_distro-0.48b0-py3-none-any.whl", hash = "sha256:b2f8fce114325b020769af3b9bf503efb8af07efc190bd1b9deac7843171664a", size = 3321, upload-time = "2024-08-28T21:26:26.584Z" }, + { url = "https://files.pythonhosted.org/packages/4c/db/806172b6a4933966eee518db814b375e620602f7fe776b74ef795690f135/opentelemetry_distro-0.49b0-py3-none-any.whl", hash = "sha256:1af4074702f605ea210753dd41947dc2fd61b39724f23cdcf15d5654867cd3c2", size = 3318, upload-time = "2024-11-05T19:20:34.065Z" }, ] [[package]] name = "opentelemetry-exporter-otlp" -version = "1.27.0" +version = "1.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/d3/8156cc14e8f4573a3572ee7f30badc7aabd02961a09acc72ab5f2c789ef1/opentelemetry_exporter_otlp-1.27.0.tar.gz", hash = "sha256:4a599459e623868cc95d933c301199c2367e530f089750e115599fccd67cb2a1", size = 6166, upload-time = "2024-08-28T21:35:33.746Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/16/14e3fc163930ea68f0980a4cdd4ae5796e60aeb898965990e13263d64baf/opentelemetry_exporter_otlp-1.28.0.tar.gz", hash = "sha256:31ae7495831681dd3da34ac457f6970f147465ae4b9aae3a888d7a581c7cd868", size = 6170, upload-time = "2024-11-05T19:14:47.349Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/6d/95e1fc2c8d945a734db32e87a5aa7a804f847c1657a21351df9338bd1c9c/opentelemetry_exporter_otlp-1.27.0-py3-none-any.whl", hash = "sha256:7688791cbdd951d71eb6445951d1cfbb7b6b2d7ee5948fac805d404802931145", size = 7001, upload-time = "2024-08-28T21:35:04.02Z" }, + { url = "https://files.pythonhosted.org/packages/c2/82/3f521b3c1f2a411ed60a24a8c9f486c1beeaf8c6c55337c87d3ae1642151/opentelemetry_exporter_otlp-1.28.0-py3-none-any.whl", hash = "sha256:1fd02d70f2c1b7ac5579c81e78de4594b188d3317c8ceb69e8b53900fb7b40fd", size = 7024, upload-time = "2024-11-05T19:14:24.534Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.27.0" +version = "1.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/2e/7eaf4ba595fb5213cf639c9158dfb64aacb2e4c7d74bfa664af89fa111f4/opentelemetry_exporter_otlp_proto_common-1.27.0.tar.gz", hash = "sha256:159d27cf49f359e3798c4c3eb8da6ef4020e292571bd8c5604a2a573231dd5c8", size = 17860, upload-time = "2024-08-28T21:35:34.896Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/8d/5d411084ac441052f4c9bae03a1aec65ae5d16b439fea7b9c5ac3842c013/opentelemetry_exporter_otlp_proto_common-1.28.0.tar.gz", hash = "sha256:5fa0419b0c8e291180b0fc8430a20dd44a3f3236f8e0827992145914f273ec4f", size = 18505, upload-time = "2024-11-05T19:14:48.204Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/27/4610ab3d9bb3cde4309b6505f98b3aabca04a26aa480aa18cede23149837/opentelemetry_exporter_otlp_proto_common-1.27.0-py3-none-any.whl", hash = "sha256:675db7fffcb60946f3a5c43e17d1168a3307a94a930ecf8d2ea1f286f3d4f79a", size = 17848, upload-time = "2024-08-28T21:35:05.412Z" }, + { url = "https://files.pythonhosted.org/packages/e1/72/3c44aabc74db325aaba09361b6a0d80f6d601f0ff86ecea8ee655c9538fc/opentelemetry_exporter_otlp_proto_common-1.28.0-py3-none-any.whl", hash = "sha256:467e6437d24e020156dffecece8c0a4471a8a60f6a34afeda7386df31a092410", size = 18403, upload-time = "2024-11-05T19:14:25.798Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.27.0" +version = "1.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecated" }, @@ -4004,14 +4034,14 @@ dependencies = [ { name = "opentelemetry-proto" }, { name = "opentelemetry-sdk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/d0/c1e375b292df26e0ffebf194e82cd197e4c26cc298582bda626ce3ce74c5/opentelemetry_exporter_otlp_proto_grpc-1.27.0.tar.gz", hash = "sha256:af6f72f76bcf425dfb5ad11c1a6d6eca2863b91e63575f89bb7b4b55099d968f", size = 26244, upload-time = "2024-08-28T21:35:36.314Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/4d/f215162e58041afb4bdf5dbd0d8faf0b7fc9bf7b3d3fc0e44e06f9e7e869/opentelemetry_exporter_otlp_proto_grpc-1.28.0.tar.gz", hash = "sha256:47a11c19dc7f4289e220108e113b7de90d59791cb4c37fc29f69a6a56f2c3735", size = 26237, upload-time = "2024-11-05T19:14:49.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/80/32217460c2c64c0568cea38410124ff680a9b65f6732867bbf857c4d8626/opentelemetry_exporter_otlp_proto_grpc-1.27.0-py3-none-any.whl", hash = "sha256:56b5bbd5d61aab05e300d9d62a6b3c134827bbd28d0b12f2649c2da368006c9e", size = 18541, upload-time = "2024-08-28T21:35:06.493Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b5/afabc8106abc0f9cfeecf5b3e682622b3e04bba1d9b967dbfcd91b9c4ebe/opentelemetry_exporter_otlp_proto_grpc-1.28.0-py3-none-any.whl", hash = "sha256:edbdc53e7783f88d4535db5807cb91bd7b1ec9e9b9cdbfee14cd378f29a3b328", size = 18532, upload-time = "2024-11-05T19:14:26.853Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.27.0" +version = "1.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecated" }, @@ -4022,28 +4052,29 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/0a/f05c55e8913bf58a033583f2580a0ec31a5f4cf2beacc9e286dcb74d6979/opentelemetry_exporter_otlp_proto_http-1.27.0.tar.gz", hash = "sha256:2103479092d8eb18f61f3fbff084f67cc7f2d4a7d37e75304b8b56c1d09ebef5", size = 15059, upload-time = "2024-08-28T21:35:37.079Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/2a/555f2845928086cd51aa6941c7a546470805b68ed631ec139ce7d841763d/opentelemetry_exporter_otlp_proto_http-1.28.0.tar.gz", hash = "sha256:d83a9a03a8367ead577f02a64127d827c79567de91560029688dd5cfd0152a8e", size = 15051, upload-time = "2024-11-05T19:14:49.813Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/8d/4755884afc0b1db6000527cac0ca17273063b6142c773ce4ecd307a82e72/opentelemetry_exporter_otlp_proto_http-1.27.0-py3-none-any.whl", hash = "sha256:688027575c9da42e179a69fe17e2d1eba9b14d81de8d13553a21d3114f3b4d75", size = 17203, upload-time = "2024-08-28T21:35:08.141Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ce/80d5adabbf7ab4a0ca7b5e0f4039b24d273be370c3ba85fc05b13794411c/opentelemetry_exporter_otlp_proto_http-1.28.0-py3-none-any.whl", hash = "sha256:e8f3f7961b747edb6b44d51de4901a61e9c01d50debd747b120a08c4996c7e7b", size = 17228, upload-time = "2024-11-05T19:14:28.613Z" }, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.48b0" +version = "0.49b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, - { name = "setuptools" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/0e/d9394839af5d55c8feb3b22cd11138b953b49739b20678ca96289e30f904/opentelemetry_instrumentation-0.48b0.tar.gz", hash = "sha256:94929685d906380743a71c3970f76b5f07476eea1834abd5dd9d17abfe23cc35", size = 24724, upload-time = "2024-08-28T21:27:42.82Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/6b/6c25b15063c92a011cf3f68375971e2c58a9c764690847edc97df2d94eeb/opentelemetry_instrumentation-0.49b0.tar.gz", hash = "sha256:398a93e0b9dc2d11cc8627e1761665c506fe08c6b2df252a2ab3ade53d751c46", size = 26478, upload-time = "2024-11-05T19:21:41.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/7f/405c41d4f359121376c9d5117dcf68149b8122d3f6c718996d037bd4d800/opentelemetry_instrumentation-0.48b0-py3-none-any.whl", hash = "sha256:a69750dc4ba6a5c3eb67986a337185a25b739966d80479befe37b546fc870b44", size = 29449, upload-time = "2024-08-28T21:26:31.288Z" }, + { url = "https://files.pythonhosted.org/packages/93/61/e0d21e958d6072ce25c4f5e26a1d22835fc86f80836660adf6badb6038ce/opentelemetry_instrumentation-0.49b0-py3-none-any.whl", hash = "sha256:68364d73a1ff40894574cbc6138c5f98674790cae1f3b0865e21cf702f24dcb3", size = 30694, upload-time = "2024-11-05T19:20:38.584Z" }, ] [[package]] name = "opentelemetry-instrumentation-asgi" -version = "0.48b0" +version = "0.49b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, @@ -4052,28 +4083,28 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/44/ac/fd3d40bab3234ec3f5c052a815100676baaae1832fa1067935f11e5c59c6/opentelemetry_instrumentation_asgi-0.48b0.tar.gz", hash = "sha256:04c32174b23c7fa72ddfe192dad874954968a6a924608079af9952964ecdf785", size = 23435, upload-time = "2024-08-28T21:27:47.276Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/55/693c3d0938ba5fead5c3aa4ac7022a992b4ff99a8e9979800d0feb843ff4/opentelemetry_instrumentation_asgi-0.49b0.tar.gz", hash = "sha256:959fd9b1345c92f20c6ef1d42f92ef6a76b3c3083fbc4104d59da6859b15b083", size = 24117, upload-time = "2024-11-05T19:21:46.769Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/74/a0e0d38622856597dd8e630f2bd793760485eb165708e11b8be1696bbb5a/opentelemetry_instrumentation_asgi-0.48b0-py3-none-any.whl", hash = "sha256:ddb1b5fc800ae66e85a4e2eca4d9ecd66367a8c7b556169d9e7b57e10676e44d", size = 15958, upload-time = "2024-08-28T21:26:38.139Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0b/7900c782a1dfaa584588d724bc3bbdf8405a32497537dd96b3fcbf8461b9/opentelemetry_instrumentation_asgi-0.49b0-py3-none-any.whl", hash = "sha256:722a90856457c81956c88f35a6db606cc7db3231046b708aae2ddde065723dbe", size = 16326, upload-time = "2024-11-05T19:20:46.176Z" }, ] [[package]] name = "opentelemetry-instrumentation-celery" -version = "0.48b0" +version = "0.49b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/68/72975eff50cc22d8f65f96c425a2e8844f91488e78ffcfb603ac7cee0e5a/opentelemetry_instrumentation_celery-0.48b0.tar.gz", hash = "sha256:1d33aa6c4a1e6c5d17a64215245208a96e56c9d07611685dbae09a557704af26", size = 14445, upload-time = "2024-08-28T21:27:56.392Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/8b/9b8a9dda3ed53354c6f707a45cdb7a4730e1c109b50fc1b413525493f811/opentelemetry_instrumentation_celery-0.49b0.tar.gz", hash = "sha256:afbaee97cc9c75f29bcc9784f16f8e37c415d4fe9b334748c5b90a3d30d12473", size = 14702, upload-time = "2024-11-05T19:21:53.672Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/59/f09e8f9f596d375fd86b7677751525bbc485c8cc8c5388e39786a3d3b968/opentelemetry_instrumentation_celery-0.48b0-py3-none-any.whl", hash = "sha256:c1904e38cc58fb2a33cd657d6e296285c5ffb0dca3f164762f94b905e5abc88e", size = 13697, upload-time = "2024-08-28T21:26:50.01Z" }, + { url = "https://files.pythonhosted.org/packages/21/8c/d7d4adb36abbc0e517a69f7a069f32742122ae22d6017202f64570d9f4c5/opentelemetry_instrumentation_celery-0.49b0-py3-none-any.whl", hash = "sha256:38d4a78c78f33020032ef77ef0ead756bdf7838bcfb603de10f5925d39f14929", size = 13749, upload-time = "2024-11-05T19:20:54.98Z" }, ] [[package]] name = "opentelemetry-instrumentation-fastapi" -version = "0.48b0" +version = "0.49b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4082,17 +4113,16 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/20/43477da5850ef2cd3792715d442aecd051e885e0603b6ee5783b2104ba8f/opentelemetry_instrumentation_fastapi-0.48b0.tar.gz", hash = "sha256:21a72563ea412c0b535815aeed75fc580240f1f02ebc72381cfab672648637a2", size = 18497, upload-time = "2024-08-28T21:28:01.14Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/bf/8e6d2a4807360f2203192017eb4845f5628dbeaf0597adf3d141cc5c24e1/opentelemetry_instrumentation_fastapi-0.49b0.tar.gz", hash = "sha256:6d14935c41fd3e49328188b6a59dd4c37bd17a66b01c15b0c64afa9714a1f905", size = 19230, upload-time = "2024-11-05T19:21:59.361Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/50/745ab075a3041b7a5f29a579d2c28eaad54f64b4589d8f9fd364c62cf0f3/opentelemetry_instrumentation_fastapi-0.48b0-py3-none-any.whl", hash = "sha256:afeb820a59e139d3e5d96619600f11ce0187658b8ae9e3480857dd790bc024f2", size = 11777, upload-time = "2024-08-28T21:26:57.457Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f4/0895b9410c10abf987c90dee1b7688a8f2214a284fe15e575648f6a1473a/opentelemetry_instrumentation_fastapi-0.49b0-py3-none-any.whl", hash = "sha256:646e1b18523cbe6860ae9711eb2c7b9c85466c3c7697cd6b8fb5180d85d3fe6e", size = 12101, upload-time = "2024-11-05T19:21:01.805Z" }, ] [[package]] name = "opentelemetry-instrumentation-flask" -version = "0.48b0" +version = "0.49b0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata" }, { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-wsgi" }, @@ -4100,29 +4130,30 @@ dependencies = [ { name = "opentelemetry-util-http" }, { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/2f/5c3af780a69f9ba78445fe0e5035c41f67281a31b08f3c3e7ec460bda726/opentelemetry_instrumentation_flask-0.48b0.tar.gz", hash = "sha256:e03a34428071aebf4864ea6c6a564acef64f88c13eb3818e64ea90da61266c3d", size = 19196, upload-time = "2024-08-28T21:28:01.986Z" } +sdist = { url = "https://files.pythonhosted.org/packages/17/12/dc72873fb1e35699941d8eb6a53ef25e8c5843dea37665dad33bd720f047/opentelemetry_instrumentation_flask-0.49b0.tar.gz", hash = "sha256:f7c5ab67753c4781a2e21c8f43dc5fc02ece74fdd819466c75d025db80aa7576", size = 19176, upload-time = "2024-11-05T19:22:00.816Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/3d/fcde4f8f0bf9fa1ee73a12304fa538076fb83fe0a2ae966ab0f0b7da5109/opentelemetry_instrumentation_flask-0.48b0-py3-none-any.whl", hash = "sha256:26b045420b9d76e85493b1c23fcf27517972423480dc6cf78fd6924248ba5808", size = 14588, upload-time = "2024-08-28T21:26:58.504Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fc/354da8f33ef0daebfc8e4eac995d342ae13a35097bbad512cfe0d2f3c61a/opentelemetry_instrumentation_flask-0.49b0-py3-none-any.whl", hash = "sha256:f3ef330c3cee3e2c161f27f1e7017c8800b9bfb6f9204f2f7bfb0b274874be0e", size = 14582, upload-time = "2024-11-05T19:21:02.793Z" }, ] [[package]] name = "opentelemetry-instrumentation-httpx" -version = "0.48b0" +version = "0.49b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, + { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d3/d9/c65d818607c16d1b7ea8d2de6111c6cecadf8d2fd38c1885a72733a7c6d3/opentelemetry_instrumentation_httpx-0.48b0.tar.gz", hash = "sha256:ee977479e10398931921fb995ac27ccdeea2e14e392cb27ef012fc549089b60a", size = 16931, upload-time = "2024-08-28T21:28:03.794Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/53/8b5e05e55a513d846ead5afb0509bec37a34a1c3e82f30b13d14156334b1/opentelemetry_instrumentation_httpx-0.49b0.tar.gz", hash = "sha256:07165b624f3e58638cee47ecf1c81939a8c2beb7e42ce9f69e25a9f21dc3f4cf", size = 17750, upload-time = "2024-11-05T19:22:02.911Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/fe/f2daa9d6d988c093b8c7b1d35df675761a8ece0b600b035dc04982746c9d/opentelemetry_instrumentation_httpx-0.48b0-py3-none-any.whl", hash = "sha256:d94f9d612c82d09fe22944d1904a30a464c19bea2ba76be656c99a28ad8be8e5", size = 13900, upload-time = "2024-08-28T21:27:01.566Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9f/843391c6d645cd4f6914b27bc807fc1ff52b97f84cbe3ca675641976b23f/opentelemetry_instrumentation_httpx-0.49b0-py3-none-any.whl", hash = "sha256:e59e0d2fda5ef841630c68da1d78ff9192f63590a9099f12f0eab614abdf239a", size = 14110, upload-time = "2024-11-05T19:21:04.698Z" }, ] [[package]] name = "opentelemetry-instrumentation-redis" -version = "0.48b0" +version = "0.49b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4130,14 +4161,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/70/be/92e98e4c7f275be3d373899a41b0a7d4df64266657d985dbbdb9a54de0d5/opentelemetry_instrumentation_redis-0.48b0.tar.gz", hash = "sha256:61e33e984b4120e1b980d9fba6e9f7ca0c8d972f9970654d8f6e9f27fa115a8c", size = 10511, upload-time = "2024-08-28T21:28:15.061Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/5b/1398eb2f92fd76787ccec28d24dc4c7dfaaf97a7557e7729e2f7c2c05d84/opentelemetry_instrumentation_redis-0.49b0.tar.gz", hash = "sha256:922542c3bd192ad4ba74e2c7e0a253c7c58a5cefbd6f89da2aba4d193a974703", size = 11353, upload-time = "2024-11-05T19:22:12.822Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/40/892f30d400091106309cc047fd3f6d76a828fedd984a953fd5386b78a2fb/opentelemetry_instrumentation_redis-0.48b0-py3-none-any.whl", hash = "sha256:48c7f2e25cbb30bde749dc0d8b9c74c404c851f554af832956b9630b27f5bcb7", size = 11610, upload-time = "2024-08-28T21:27:18.759Z" }, + { url = "https://files.pythonhosted.org/packages/24/e4/4f258fef0759629f2e8a0210d5533cfef3ecad69ff35be044637a3e2783e/opentelemetry_instrumentation_redis-0.49b0-py3-none-any.whl", hash = "sha256:b7d8f758bac53e77b7e7ca98ce80f91230577502dacb619ebe8e8b6058042067", size = 12453, upload-time = "2024-11-05T19:21:18.534Z" }, ] [[package]] name = "opentelemetry-instrumentation-sqlalchemy" -version = "0.48b0" +version = "0.49b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4146,14 +4177,14 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/77/3fcebbca8bd729da50dc2130d8ca869a235aa5483a85ef06c5dc8643476b/opentelemetry_instrumentation_sqlalchemy-0.48b0.tar.gz", hash = "sha256:dbf2d5a755b470e64e5e2762b56f8d56313787e4c7d71a87fe25c33f48eb3493", size = 13194, upload-time = "2024-08-28T21:28:18.122Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/a7/24f6cce3808ae1802dd1b60d752fbab877db5655198929cf4ee8ea416923/opentelemetry_instrumentation_sqlalchemy-0.49b0.tar.gz", hash = "sha256:32658e520fc8b35823c722f5d8831d3a410b76dd2724adb2887befc041ddef04", size = 13194, upload-time = "2024-11-05T19:22:14.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/84/4b6f1e9e9f83a52d966e91963f5a8424edc4a3d5ea32854c96c2d1618284/opentelemetry_instrumentation_sqlalchemy-0.48b0-py3-none-any.whl", hash = "sha256:625848a34aa5770cb4b1dcdbd95afce4307a0230338711101325261d739f391f", size = 13360, upload-time = "2024-08-28T21:27:22.102Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/a1a3685fed593282999cdc374ece15efbd56f8d774bd368bf7ff2cf5923c/opentelemetry_instrumentation_sqlalchemy-0.49b0-py3-none-any.whl", hash = "sha256:d854052d2b02cd0562e5628a514c8153fceada7f585137e173165dfd0a46ef6a", size = 13358, upload-time = "2024-11-05T19:21:23.654Z" }, ] [[package]] name = "opentelemetry-instrumentation-wsgi" -version = "0.48b0" +version = "0.49b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4161,70 +4192,70 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/a5/f45cdfba18f22aefd2378eac8c07c1f8c9656d6bf7ce315ced48c67f3437/opentelemetry_instrumentation_wsgi-0.48b0.tar.gz", hash = "sha256:1a1e752367b0df4397e0b835839225ef5c2c3c053743a261551af13434fc4d51", size = 17974, upload-time = "2024-08-28T21:28:24.902Z" } +sdist = { url = "https://files.pythonhosted.org/packages/17/2b/91b022b004ac9e9ab0eefd10bc4257975291f88adc81b4ef2c601ddb1adf/opentelemetry_instrumentation_wsgi-0.49b0.tar.gz", hash = "sha256:0812a02e132f8fc3d5c897bba84e530c37b85c315b199bb97ca6508279e7eb23", size = 17733, upload-time = "2024-11-05T19:22:24.3Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/87/fa420007e0ba7e8cd43799ab204717ab515f000236fa2726a6be3299efdd/opentelemetry_instrumentation_wsgi-0.48b0-py3-none-any.whl", hash = "sha256:c6051124d741972090fe94b2fa302555e1e2a22e9cdda32dd39ed49a5b34e0c6", size = 13691, upload-time = "2024-08-28T21:27:33.257Z" }, + { url = "https://files.pythonhosted.org/packages/02/1d/59979665778ed8c85bc31c92b75571cd7afb8e3322fb513c87fe1bad6d78/opentelemetry_instrumentation_wsgi-0.49b0-py3-none-any.whl", hash = "sha256:8869ccf96611827e4448417718920e9eec6d25bffb5bf72c7952c7346ec33fbc", size = 13699, upload-time = "2024-11-05T19:21:35.039Z" }, ] [[package]] name = "opentelemetry-propagator-b3" -version = "1.27.0" +version = "1.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecated" }, { name = "opentelemetry-api" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/a3/3ceeb5ff5a1906371834d5c594e24e5b84f35528d219054833deca4ac44c/opentelemetry_propagator_b3-1.27.0.tar.gz", hash = "sha256:39377b6aa619234e08fbc6db79bf880aff36d7e2761efa9afa28b78d5937308f", size = 9590, upload-time = "2024-08-28T21:35:43.971Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/1d/225ea036785119964509e92f4e1bc0313ba6ec790fbf51bd363abafeafae/opentelemetry_propagator_b3-1.28.0.tar.gz", hash = "sha256:cf6f0d2a1881c4858898be47e8a94b11bc5b16fc73b6c37ebfa2121c4825adc6", size = 9592, upload-time = "2024-11-05T19:14:57.193Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/3f/75ba77b8d9938bae575bc457a5c56ca2246ff5367b54c7d4252a31d1c91f/opentelemetry_propagator_b3-1.27.0-py3-none-any.whl", hash = "sha256:1dd75e9801ba02e870df3830097d35771a64c123127c984d9b05c352a35aa9cc", size = 8899, upload-time = "2024-08-28T21:35:18.317Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fa/438d53d73a6c45df5d416b56dc371a65d0b07859bc107ab632349a079d4a/opentelemetry_propagator_b3-1.28.0-py3-none-any.whl", hash = "sha256:9f6923a5da56d7da6724e4fdd758a67ede2a2732efb929e538cf6fea337700c5", size = 8917, upload-time = "2024-11-05T19:14:37.317Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.27.0" +version = "1.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/59/959f0beea798ae0ee9c979b90f220736fbec924eedbefc60ca581232e659/opentelemetry_proto-1.27.0.tar.gz", hash = "sha256:33c9345d91dafd8a74fc3d7576c5a38f18b7fdf8d02983ac67485386132aedd6", size = 34749, upload-time = "2024-08-28T21:35:45.839Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/63/ac4cef4d30ea0ca1d2153ad2fc62d91d1cf3b89b0e4e5cbd61a8c567885f/opentelemetry_proto-1.28.0.tar.gz", hash = "sha256:4a45728dfefa33f7908b828b9b7c9f2c6de42a05d5ec7b285662ddae71c4c870", size = 34331, upload-time = "2024-11-05T19:14:59.503Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/56/3d2d826834209b19a5141eed717f7922150224d1a982385d19a9444cbf8d/opentelemetry_proto-1.27.0-py3-none-any.whl", hash = "sha256:b133873de5581a50063e1e4b29cdcf0c5e253a8c2d8dc1229add20a4c3830ace", size = 52464, upload-time = "2024-08-28T21:35:21.434Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/c0b43d16e1d96ee1e699373aa59f14a3aa2e7126af3f11d6adc5dcc531cd/opentelemetry_proto-1.28.0-py3-none-any.whl", hash = "sha256:d5ad31b997846543b8e15504657d9a8cf1ad3c71dcbbb6c4799b1ab29e38f7f9", size = 55832, upload-time = "2024-11-05T19:14:40.446Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.27.0" +version = "1.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/9a/82a6ac0f06590f3d72241a587cb8b0b751bd98728e896cc4cbd4847248e6/opentelemetry_sdk-1.27.0.tar.gz", hash = "sha256:d525017dea0ccce9ba4e0245100ec46ecdc043f2d7b8315d56b19aff0904fa6f", size = 145019, upload-time = "2024-08-28T21:35:46.708Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/5b/a509ccab93eacc6044591d5ec437d8266e76f893d0389bbf7e5592c7da32/opentelemetry_sdk-1.28.0.tar.gz", hash = "sha256:41d5420b2e3fb7716ff4981b510d551eff1fc60eb5a95cf7335b31166812a893", size = 156155, upload-time = "2024-11-05T19:15:00.451Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/bd/a6602e71e315055d63b2ff07172bd2d012b4cba2d4e00735d74ba42fc4d6/opentelemetry_sdk-1.27.0-py3-none-any.whl", hash = "sha256:365f5e32f920faf0fd9e14fdfd92c086e317eaa5f860edba9cdc17a380d9197d", size = 110505, upload-time = "2024-08-28T21:35:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fe/c8decbebb5660529f1d6ba65e50a45b1294022dfcba2968fc9c8697c42b2/opentelemetry_sdk-1.28.0-py3-none-any.whl", hash = "sha256:4b37da81d7fad67f6683c4420288c97f4ed0d988845d5886435f428ec4b8429a", size = 118692, upload-time = "2024-11-05T19:14:41.669Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.48b0" +version = "0.49b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecated" }, { name = "opentelemetry-api" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/89/1724ad69f7411772446067cdfa73b598694c8c91f7f8c922e344d96d81f9/opentelemetry_semantic_conventions-0.48b0.tar.gz", hash = "sha256:12d74983783b6878162208be57c9effcb89dc88691c64992d70bb89dc00daa1a", size = 89445, upload-time = "2024-08-28T21:35:47.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/c8/433b0e54143f8c9369f5c4a7a83e73eec7eb2ee7d0b7e81a9243e78c8e80/opentelemetry_semantic_conventions-0.49b0.tar.gz", hash = "sha256:dbc7b28339e5390b6b28e022835f9bac4e134a80ebf640848306d3c5192557e8", size = 95227, upload-time = "2024-11-05T19:15:01.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/7a/4f0063dbb0b6c971568291a8bc19a4ca70d3c185db2d956230dd67429dfc/opentelemetry_semantic_conventions-0.48b0-py3-none-any.whl", hash = "sha256:a0de9f45c413a8669788a38569c7e0a11ce6ce97861a628cca785deecdc32a1f", size = 149685, upload-time = "2024-08-28T21:35:25.983Z" }, + { url = "https://files.pythonhosted.org/packages/25/05/20104df4ef07d3bf5c3fd6bcc796ef70ab4ea4309378a9ba57bc4b4d01fa/opentelemetry_semantic_conventions-0.49b0-py3-none-any.whl", hash = "sha256:0458117f6ead0b12e3221813e3e511d85698c31901cac84682052adb9c17c7cd", size = 159214, upload-time = "2024-11-05T19:14:43.047Z" }, ] [[package]] name = "opentelemetry-util-http" -version = "0.48b0" +version = "0.49b0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/d7/185c494754340e0a3928fd39fde2616ee78f2c9d66253affaad62d5b7935/opentelemetry_util_http-0.48b0.tar.gz", hash = "sha256:60312015153580cc20f322e5cdc3d3ecad80a71743235bdb77716e742814623c", size = 7863, upload-time = "2024-08-28T21:28:27.266Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/99/377ef446928808211b127b9ab31c348bc465c8da4514ebeec6e4a3de3d21/opentelemetry_util_http-0.49b0.tar.gz", hash = "sha256:02928496afcffd58a7c15baf99d2cedae9b8325a8ac52b0d0877b2e8f936dd1b", size = 7863, upload-time = "2024-11-05T19:22:26.973Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/2e/36097c0a4d0115b8c7e377c90bab7783ac183bc5cb4071308f8959454311/opentelemetry_util_http-0.48b0-py3-none-any.whl", hash = "sha256:76f598af93aab50328d2a69c786beaedc8b6a7770f7a818cc307eb353debfffb", size = 6946, upload-time = "2024-08-28T21:27:37.975Z" }, + { url = "https://files.pythonhosted.org/packages/66/0e/ab0a89b315d0bacdd355a345bb69b20c50fc1f0804b52b56fe1c35a60e68/opentelemetry_util_http-0.49b0-py3-none-any.whl", hash = "sha256:8661bbd6aea1839badc44de067ec9c15c05eab05f729f496c856c50a1203caf1", size = 6945, upload-time = "2024-11-05T19:21:37.81Z" }, ] [[package]] @@ -4670,16 +4701,16 @@ wheels = [ [[package]] name = "protobuf" -version = "4.25.8" +version = "5.29.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/01/34c8d2b6354906d728703cb9d546a0e534de479e25f1b581e4094c4a85cc/protobuf-4.25.8.tar.gz", hash = "sha256:6135cf8affe1fc6f76cced2641e4ea8d3e59518d1f24ae41ba97bcad82d397cd", size = 380920, upload-time = "2025-05-28T14:22:25.153Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/57/394a763c103e0edf87f0938dafcd918d53b4c011dfc5c8ae80f3b0452dbb/protobuf-5.29.6.tar.gz", hash = "sha256:da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723", size = 425623, upload-time = "2026-02-04T22:54:40.584Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/ff/05f34305fe6b85bbfbecbc559d423a5985605cad5eda4f47eae9e9c9c5c5/protobuf-4.25.8-cp310-abi3-win32.whl", hash = "sha256:504435d831565f7cfac9f0714440028907f1975e4bed228e58e72ecfff58a1e0", size = 392745, upload-time = "2025-05-28T14:22:10.524Z" }, - { url = "https://files.pythonhosted.org/packages/08/35/8b8a8405c564caf4ba835b1fdf554da869954712b26d8f2a98c0e434469b/protobuf-4.25.8-cp310-abi3-win_amd64.whl", hash = "sha256:bd551eb1fe1d7e92c1af1d75bdfa572eff1ab0e5bf1736716814cdccdb2360f9", size = 413736, upload-time = "2025-05-28T14:22:13.156Z" }, - { url = "https://files.pythonhosted.org/packages/28/d7/ab27049a035b258dab43445eb6ec84a26277b16105b277cbe0a7698bdc6c/protobuf-4.25.8-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:ca809b42f4444f144f2115c4c1a747b9a404d590f18f37e9402422033e464e0f", size = 394537, upload-time = "2025-05-28T14:22:14.768Z" }, - { url = "https://files.pythonhosted.org/packages/bd/6d/a4a198b61808dd3d1ee187082ccc21499bc949d639feb948961b48be9a7e/protobuf-4.25.8-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:9ad7ef62d92baf5a8654fbb88dac7fa5594cfa70fd3440488a5ca3bfc6d795a7", size = 294005, upload-time = "2025-05-28T14:22:16.052Z" }, - { url = "https://files.pythonhosted.org/packages/d6/c6/c9deaa6e789b6fc41b88ccbdfe7a42d2b82663248b715f55aa77fbc00724/protobuf-4.25.8-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:83e6e54e93d2b696a92cad6e6efc924f3850f82b52e1563778dfab8b355101b0", size = 294924, upload-time = "2025-05-28T14:22:17.105Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c1/6aece0ab5209981a70cd186f164c133fdba2f51e124ff92b73de7fd24d78/protobuf-4.25.8-py3-none-any.whl", hash = "sha256:15a0af558aa3b13efef102ae6e4f3efac06f1eea11afb3a57db2901447d9fb59", size = 156757, upload-time = "2025-05-28T14:22:24.135Z" }, + { url = "https://files.pythonhosted.org/packages/d4/88/9ee58ff7863c479d6f8346686d4636dd4c415b0cbeed7a6a7d0617639c2a/protobuf-5.29.6-cp310-abi3-win32.whl", hash = "sha256:62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1", size = 423357, upload-time = "2026-02-04T22:54:25.805Z" }, + { url = "https://files.pythonhosted.org/packages/1c/66/2dc736a4d576847134fb6d80bd995c569b13cdc7b815d669050bf0ce2d2c/protobuf-5.29.6-cp310-abi3-win_amd64.whl", hash = "sha256:7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda", size = 435175, upload-time = "2026-02-04T22:54:28.592Z" }, + { url = "https://files.pythonhosted.org/packages/06/db/49b05966fd208ae3f44dcd33837b6243b4915c57561d730a43f881f24dea/protobuf-5.29.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269", size = 418619, upload-time = "2026-02-04T22:54:30.266Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d7/48cbf6b0c3c39761e47a99cb483405f0fde2be22cf00d71ef316ce52b458/protobuf-5.29.6-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6", size = 320284, upload-time = "2026-02-04T22:54:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dd/cadd6ec43069247d91f6345fa7a0d2858bef6af366dbd7ba8f05d2c77d3b/protobuf-5.29.6-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9", size = 320478, upload-time = "2026-02-04T22:54:32.909Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cb/e3065b447186cb70aa65acc70c86baf482d82bf75625bf5a2c4f6919c6a3/protobuf-5.29.6-py3-none-any.whl", hash = "sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86", size = 173126, upload-time = "2026-02-04T22:54:39.462Z" }, ] [[package]] @@ -5762,33 +5793,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, ] -[[package]] -name = "shapely" -version = "2.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038, upload-time = "2025-09-24T13:50:15.628Z" }, - { url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039, upload-time = "2025-09-24T13:50:16.881Z" }, - { url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519, upload-time = "2025-09-24T13:50:18.606Z" }, - { url = "https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26", size = 3110842, upload-time = "2025-09-24T13:50:21.77Z" }, - { url = "https://files.pythonhosted.org/packages/af/61/8e389c97994d5f331dcffb25e2fa761aeedfb52b3ad9bcdd7b8671f4810a/shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7", size = 4021316, upload-time = "2025-09-24T13:50:23.626Z" }, - { url = "https://files.pythonhosted.org/packages/d3/d4/9b2a9fe6039f9e42ccf2cb3e84f219fd8364b0c3b8e7bbc857b5fbe9c14c/shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2", size = 4178586, upload-time = "2025-09-24T13:50:25.443Z" }, - { url = "https://files.pythonhosted.org/packages/16/f6/9840f6963ed4decf76b08fd6d7fed14f8779fb7a62cb45c5617fa8ac6eab/shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6", size = 1543961, upload-time = "2025-09-24T13:50:26.968Z" }, - { url = "https://files.pythonhosted.org/packages/38/1e/3f8ea46353c2a33c1669eb7327f9665103aa3a8dfe7f2e4ef714c210b2c2/shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc", size = 1722856, upload-time = "2025-09-24T13:50:28.497Z" }, - { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" }, - { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" }, - { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" }, - { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844, upload-time = "2025-09-24T13:50:35.459Z" }, - { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842, upload-time = "2025-09-24T13:50:37.478Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714, upload-time = "2025-09-24T13:50:39.9Z" }, - { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745, upload-time = "2025-09-24T13:50:41.414Z" }, - { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" }, -] - [[package]] name = "shellingham" version = "1.5.4"