From d8781d90f1fc9d5dcabe57643292ad7b6ee571b2 Mon Sep 17 00:00:00 2001 From: "const.koutsakis@aurecongroup.com" Date: Mon, 27 Apr 2026 03:31:51 +1000 Subject: [PATCH] feat: observability setup (OTel SDK, OTLP exporter, structured logging, span helpers) (#19) --- pyproject.toml | 6 + src/api/main.py | 10 ++ src/observability/logging.py | 54 ++++++++ src/observability/spans.py | 84 ++++++++++++ src/observability/tracing.py | 75 ++++++++++ tests/test_observability.py | 124 +++++++++++++++++ uv.lock | 257 +++++++++++++++++++++++++++++++++++ 7 files changed, 610 insertions(+) create mode 100644 src/observability/logging.py create mode 100644 src/observability/spans.py create mode 100644 src/observability/tracing.py create mode 100644 tests/test_observability.py diff --git a/pyproject.toml b/pyproject.toml index aae2e82..ae42e70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,12 @@ dependencies = [ "pydantic>=2.11.0", "pydantic-settings>=2.9.0", "httpx>=0.28.1", + "opentelemetry-api>=1.33.0", + "opentelemetry-sdk>=1.33.0", + "opentelemetry-exporter-otlp-proto-grpc>=1.33.0", + "opentelemetry-instrumentation-fastapi>=0.62b0", + "opentelemetry-instrumentation-httpx>=0.54b0", + "opentelemetry-instrumentation-logging>=0.54b0", ] [project.optional-dependencies] diff --git a/src/api/main.py b/src/api/main.py index e8f97ad..2ac996a 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -12,6 +12,12 @@ from src.api.routes import router as v1_router from src.api.sessions import SessionStore +from src.observability.logging import setup_logging +from src.observability.tracing import ( + instrument_fastapi, + instrument_httpx, + setup_tracing, +) if TYPE_CHECKING: from collections.abc import AsyncIterator @@ -36,6 +42,10 @@ def _package_version() -> str: @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncIterator[None]: """Application lifespan: initialise process-wide services on startup.""" + setup_tracing() + setup_logging() + instrument_httpx() + instrument_fastapi(app) app.state.session_store = SessionStore() logger.info("harness-python-react API started (v%s)", _package_version()) yield diff --git a/src/observability/logging.py b/src/observability/logging.py new file mode 100644 index 0000000..5cbdb06 --- /dev/null +++ b/src/observability/logging.py @@ -0,0 +1,54 @@ +"""Structured JSON logging with OpenTelemetry trace correlation.""" + +from __future__ import annotations + +import json +import logging +import os +import sys +from datetime import UTC, datetime + + +class _JSONFormatter(logging.Formatter): + """Format log records as single-line JSON with OTel trace context.""" + + def format(self, record: logging.LogRecord) -> str: + entry: dict[str, str | int | float] = { + "timestamp": datetime.fromtimestamp(record.created, tz=UTC).isoformat(), + "level": record.levelname, + "module": record.module, + "message": record.getMessage(), + } + + # Trace context injected by OTel logging instrumentation + entry["trace_id"] = getattr(record, "otelTraceID", "0") + entry["span_id"] = getattr(record, "otelSpanID", "0") + + return json.dumps(entry, default=str) + + +def setup_logging(level: str | None = None) -> None: + """Configure Python logging with structured JSON output and OTel correlation. + + Reads ``LOG_LEVEL`` from the environment if *level* is not provided. + + Args: + level: Logging level name (e.g. "INFO", "DEBUG"). Falls back to the + ``LOG_LEVEL`` environment variable, then to ``"INFO"``. + """ + from opentelemetry.instrumentation.logging import LoggingInstrumentor + + resolved_level = (level if level else os.getenv("LOG_LEVEL", "INFO")).upper() + + # Instrument stdlib logging so trace/span IDs are injected + LoggingInstrumentor().instrument(set_logging_format=False) + + root = logging.getLogger() + root.setLevel(resolved_level) + + # Remove existing handlers to avoid duplicate output + root.handlers.clear() + + handler = logging.StreamHandler(sys.stderr) + handler.setFormatter(_JSONFormatter()) + root.addHandler(handler) diff --git a/src/observability/spans.py b/src/observability/spans.py new file mode 100644 index 0000000..a9a9dde --- /dev/null +++ b/src/observability/spans.py @@ -0,0 +1,84 @@ +"""Span helper utilities and OTel semantic-convention attribute keys. + +Use semconv-defined attribute names where one exists. The full GenAI registry +is at https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/ +and the DB registry at https://opentelemetry.io/docs/specs/semconv/database/. + +Semconv-stable attribute keys live as module constants so a typo at the +call site is a NameError, not a silently-different attribute. +""" + +from __future__ import annotations + +from contextlib import contextmanager +from typing import TYPE_CHECKING + +from opentelemetry import trace + +if TYPE_CHECKING: + from collections.abc import Iterator, Mapping + +# --------------------------------------------------------------------------- +# GenAI semantic convention attribute keys +# https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/ +# --------------------------------------------------------------------------- + +# GenAI — conversation & request +GENAI_CONVERSATION_ID: str = "gen_ai.conversation.id" +GENAI_REQUEST_MODEL: str = "gen_ai.request.model" +GENAI_OPERATION_NAME: str = "gen_ai.operation.name" +GENAI_PROVIDER_NAME: str = "gen_ai.provider.name" + +# GenAI — usage (token counts) +GENAI_USAGE_INPUT_TOKENS: str = "gen_ai.usage.input_tokens" +GENAI_USAGE_OUTPUT_TOKENS: str = "gen_ai.usage.output_tokens" + +# GenAI — tool calling +GENAI_TOOL_NAME: str = "gen_ai.tool.name" +GENAI_TOOL_CALL_ARGUMENTS: str = "gen_ai.tool.call.arguments" +GENAI_TOOL_CALL_RESULT: str = "gen_ai.tool.call.result" + +# DB semantic convention attribute keys +# https://opentelemetry.io/docs/specs/semconv/database/ +DB_QUERY_TEXT: str = "db.query.text" +DB_RESPONSE_RETURNED_ROWS: str = "db.response.returned_rows" + + +# --------------------------------------------------------------------------- +# Span helpers +# --------------------------------------------------------------------------- + + +@contextmanager +def agent_span( + name: str, + attributes: Mapping[str, str | int | float | bool] | None = None, +) -> Iterator[trace.Span]: + """Create a span and yield it for further mutation. + + Args: + name: Human-readable span name. + attributes: Initial key-value attributes to set on the span. + """ + tracer = trace.get_tracer(__name__) + with tracer.start_as_current_span(name) as span: + if attributes: + for key, value in attributes.items(): + span.set_attribute(key, value) + yield span + + +def set_span_attributes( + span: trace.Span, + **kwargs: str | int | float | bool | None, +) -> None: + """Set multiple attributes on a span, filtering ``None`` values. + + Args: + span: The span to annotate. + **kwargs: Attribute key-value pairs. ``None`` values are silently + skipped. + """ + for key, value in kwargs.items(): + if value is not None: + span.set_attribute(key, value) diff --git a/src/observability/tracing.py b/src/observability/tracing.py new file mode 100644 index 0000000..e9f9aef --- /dev/null +++ b/src/observability/tracing.py @@ -0,0 +1,75 @@ +"""Tracer provider setup, exporters, and auto-instrumentation.""" + +from __future__ import annotations + +import os +from typing import TYPE_CHECKING + +from opentelemetry import trace +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, + ConsoleSpanExporter, +) + +if TYPE_CHECKING: + from fastapi import FastAPI + + +def setup_tracing() -> TracerProvider: + """Create and configure the global tracer provider. + + Reads configuration from environment variables: + + - ``OTEL_SERVICE_NAME`` — Resource ``service.name`` attribute. + Default ``harness-python-react``. + - ``OTEL_EXPORTER`` — ``otlp`` (default) or ``console``. + - ``OTEL_EXPORTER_OTLP_ENDPOINT`` — gRPC endpoint for OTLP exporter + (default ``http://localhost:4317``; + Jaeger via docker-compose). + + Returns the configured TracerProvider. + """ + service_name = os.getenv("OTEL_SERVICE_NAME", "harness-python-react") + resource = Resource.create({"service.name": service_name}) + provider = TracerProvider(resource=resource) + + exporter_type = os.getenv("OTEL_EXPORTER", "otlp") + + if exporter_type == "console": + provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) + else: + endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317") + from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter, + ) + + exporter = OTLPSpanExporter(endpoint=endpoint, insecure=True) + provider.add_span_processor(BatchSpanProcessor(exporter)) + + trace.set_tracer_provider(provider) + return provider + + +def get_tracer(name: str) -> trace.Tracer: + """Get a tracer from the global provider. + + Args: + name: Logical name for the tracer, typically the module path. + """ + return trace.get_tracer(name) + + +def instrument_fastapi(app: FastAPI) -> None: + """Apply OpenTelemetry auto-instrumentation to a FastAPI application.""" + from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor + + FastAPIInstrumentor.instrument_app(app) + + +def instrument_httpx() -> None: + """Apply OpenTelemetry auto-instrumentation to httpx HTTP clients.""" + from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor + + HTTPXClientInstrumentor().instrument() diff --git a/tests/test_observability.py b/tests/test_observability.py new file mode 100644 index 0000000..4cf0983 --- /dev/null +++ b/tests/test_observability.py @@ -0,0 +1,124 @@ +"""Tests for src/observability/ — tracing, logging, span helpers.""" + +from __future__ import annotations + +import json +import logging +from io import StringIO + +import pytest +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + +from src.observability.logging import _JSONFormatter, setup_logging +from src.observability.spans import ( + GENAI_CONVERSATION_ID, + GENAI_OPERATION_NAME, + GENAI_REQUEST_MODEL, + GENAI_TOOL_NAME, + agent_span, + set_span_attributes, +) + + +@pytest.fixture() +def exporter() -> InMemorySpanExporter: + """Attach an in-memory span exporter to the (possibly pre-existing) + global tracer provider. Clears captured spans on entry so each test + sees a clean slate even when OTel's provider override has already + been set by a prior test.""" + captured = InMemorySpanExporter() + provider = trace.get_tracer_provider() + if not isinstance(provider, TracerProvider): + provider = TracerProvider() + trace.set_tracer_provider(provider) + provider.add_span_processor(SimpleSpanProcessor(captured)) + captured.clear() + return captured + + +def test_agent_span_records(exporter: InMemorySpanExporter) -> None: + with agent_span("op", attributes={GENAI_OPERATION_NAME: "echo"}): + pass + + spans = exporter.get_finished_spans() + assert len(spans) == 1 + assert spans[0].name == "op" + attrs = dict(spans[0].attributes or {}) + assert attrs[GENAI_OPERATION_NAME] == "echo" + + +def test_set_span_attributes_skips_none(exporter: InMemorySpanExporter) -> None: + with agent_span("op") as span: + set_span_attributes( + span, + **{ + GENAI_REQUEST_MODEL: "gpt-4o-mini", + GENAI_CONVERSATION_ID: None, + GENAI_TOOL_NAME: "echo_tool", + }, + ) + + spans = exporter.get_finished_spans() + attrs = dict(spans[0].attributes or {}) + assert attrs[GENAI_REQUEST_MODEL] == "gpt-4o-mini" + assert attrs[GENAI_TOOL_NAME] == "echo_tool" + assert GENAI_CONVERSATION_ID not in attrs + + +def test_semconv_attributes_have_dotted_names() -> None: + """Sanity check: every exported semconv key uses the official dotted form.""" + for key in ( + GENAI_CONVERSATION_ID, + GENAI_REQUEST_MODEL, + GENAI_OPERATION_NAME, + GENAI_TOOL_NAME, + ): + assert key.startswith("gen_ai.") + + +def test_json_formatter_emits_trace_and_span_ids() -> None: + formatter = _JSONFormatter() + record = logging.LogRecord( + name="test", + level=logging.INFO, + pathname="test.py", + lineno=1, + msg="hello %s", + args=("world",), + exc_info=None, + ) + record.otelTraceID = "abc123" + record.otelSpanID = "def456" + + payload = json.loads(formatter.format(record)) + assert payload["message"] == "hello world" + assert payload["trace_id"] == "abc123" + assert payload["span_id"] == "def456" + + +def test_setup_logging_attaches_json_handler( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("LOG_LEVEL", "DEBUG") + setup_logging() + + root = logging.getLogger() + assert root.level == logging.DEBUG + assert any(isinstance(h.formatter, _JSONFormatter) for h in root.handlers) + + # Round-trip: emit a log record and assert the JSON is single-line valid. + handler = next(h for h in root.handlers if isinstance(h.formatter, _JSONFormatter)) + assert isinstance(handler, logging.StreamHandler) + buf = StringIO() + handler.setStream(buf) + root.info("ping") + line = buf.getvalue().strip() + assert "\n" not in line + parsed = json.loads(line) + assert parsed["message"] == "ping" + assert parsed["level"] == "INFO" diff --git a/uv.lock b/uv.lock index ec07200..b6c0c1e 100644 --- a/uv.lock +++ b/uv.lock @@ -45,6 +45,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, ] +[[package]] +name = "asgiref" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, +] + [[package]] name = "certifi" version = "2026.4.22" @@ -242,6 +251,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, ] +[[package]] +name = "googleapis-common-protos" +version = "1.74.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/18/a746c8344152d368a5aac738d4c857012f2c5d1fd2eac7e17b647a7861bd/googleapis_common_protos-1.74.0.tar.gz", hash = "sha256:57971e4eeeba6aad1163c1f0fc88543f965bb49129b8bb55b2b7b26ecab084f1", size = 151254, upload-time = "2026-04-02T21:23:26.679Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/b0/be5d3329badb9230b765de6eea66b73abd5944bdeb5afb3562ddcd80ae84/googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5", size = 300743, upload-time = "2026-04-02T21:22:49.108Z" }, +] + [[package]] name = "grimp" version = "3.14" @@ -275,6 +296,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/8f/774ce522de6a7e70fbeceeaeb6fbe502f5dfb8365728fb3bb4cb23463da8/grimp-3.14-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a424ad14d5deb56721ac24ab939747f72ab3d378d42e7d1f038317d33b052b77", size = 2515157, upload-time = "2025-12-10T17:54:55.874Z" }, ] +[[package]] +name = "grpcio" +version = "1.80.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/6d/e65307ce20f5a09244ba9e9d8476e99fb039de7154f37fb85f26978b59c3/grpcio-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3d4147a97c8344d065d01bbf8b6acec2cf86fb0400d40696c8bdad34a64ffc0e", size = 6017376, upload-time = "2026-03-30T08:48:10.005Z" }, + { url = "https://files.pythonhosted.org/packages/69/10/9cef5d9650c72625a699c549940f0abb3c4bfdb5ed45a5ce431f92f31806/grpcio-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8e11f167935b3eb089ac9038e1a063e6d7dbe995c0bb4a661e614583352e76f", size = 12018133, upload-time = "2026-03-30T08:48:12.927Z" }, + { url = "https://files.pythonhosted.org/packages/04/82/983aabaad82ba26113caceeb9091706a0696b25da004fe3defb5b346e15b/grpcio-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f14b618fc30de822681ee986cfdcc2d9327229dc4c98aed16896761cacd468b9", size = 6574748, upload-time = "2026-03-30T08:48:16.386Z" }, + { url = "https://files.pythonhosted.org/packages/07/d7/031666ef155aa0bf399ed7e19439656c38bbd143779ae0861b038ce82abd/grpcio-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4ed39fbdcf9b87370f6e8df4e39ca7b38b3e5e9d1b0013c7b6be9639d6578d14", size = 7277711, upload-time = "2026-03-30T08:48:19.627Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/f437a78f7f4f1d311804189e8f11fb311a01049b2e08557c1068d470cb2e/grpcio-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2dcc70e9f0ba987526e8e8603a610fb4f460e42899e74e7a518bf3c68fe1bf05", size = 6785372, upload-time = "2026-03-30T08:48:22.373Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/f6558e9c6296cb4227faa5c43c54a34c68d32654b829f53288313d16a86e/grpcio-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448c884b668b868562b1bda833c5fce6272d26e1926ec46747cda05741d302c1", size = 7395268, upload-time = "2026-03-30T08:48:25.638Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/0fdd77e84720b08843c371a2efa6f2e19dbebf56adc72df73d891f5506f0/grpcio-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a1dc80fe55685b4a543555e6eef975303b36c8db1023b1599b094b92aa77965f", size = 8392000, upload-time = "2026-03-30T08:48:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/f5/68/67f4947ed55d2e69f2cc199ab9fd85e0a0034d813bbeef84df6d2ba4d4b7/grpcio-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:31b9ac4ad1aa28ffee5503821fafd09e4da0a261ce1c1281c6c8da0423c83b6e", size = 7828477, upload-time = "2026-03-30T08:48:32.054Z" }, + { url = "https://files.pythonhosted.org/packages/44/b6/8d4096691b2e385e8271911a0de4f35f0a6c7d05aff7098e296c3de86939/grpcio-1.80.0-cp314-cp314-win32.whl", hash = "sha256:367ce30ba67d05e0592470428f0ec1c31714cab9ef19b8f2e37be1f4c7d32fae", size = 4218563, upload-time = "2026-03-30T08:48:34.538Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8c/bbe6baf2557262834f2070cf668515fa308b2d38a4bbf771f8f7872a7036/grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f", size = 5019457, upload-time = "2026-03-30T08:48:37.308Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -291,6 +333,12 @@ source = { virtual = "." } dependencies = [ { name = "fastapi" }, { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-instrumentation-fastapi" }, + { name = "opentelemetry-instrumentation-httpx" }, + { name = "opentelemetry-instrumentation-logging" }, + { name = "opentelemetry-sdk" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "uvicorn", extra = ["standard"] }, @@ -317,6 +365,12 @@ requires-dist = [ { name = "httpx", specifier = ">=0.28.1" }, { name = "import-linter", marker = "extra == 'dev'", specifier = ">=2.0.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.15.0" }, + { name = "opentelemetry-api", specifier = ">=1.33.0" }, + { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.33.0" }, + { name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.62b0" }, + { name = "opentelemetry-instrumentation-httpx", specifier = ">=0.54b0" }, + { name = "opentelemetry-instrumentation-logging", specifier = ">=0.54b0" }, + { name = "opentelemetry-sdk", specifier = ">=1.33.0" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.0.0" }, { name = "pydantic", specifier = ">=2.11.0" }, { name = "pydantic-settings", specifier = ">=2.9.0" }, @@ -406,6 +460,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/aa/2ed2c89543632ded7196e0d93dcc6c7fe87769e88391a648c4a298ea864a/import_linter-2.11-py3-none-any.whl", hash = "sha256:3dc54cae933bae3430358c30989762b721c77aa99d424f56a08265be0eeaa465", size = 637315, upload-time = "2026-03-06T12:11:36.599Z" }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -559,6 +625,173 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] +[[package]] +name = "opentelemetry-api" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/fc/b7564cbef36601aef0d6c9bc01f7badb64be8e862c2e1c3c5c3b43b53e4f/opentelemetry_api-1.41.1.tar.gz", hash = "sha256:0ad1814d73b875f84494387dae86ce0b12c68556331ce6ce8fe789197c949621", size = 71416, upload-time = "2026-04-24T13:15:38.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/59/3e7118ed140f76b0982ba4321bdaed1997a0473f9720de2d10788a577033/opentelemetry_api-1.41.1-py3-none-any.whl", hash = "sha256:a22df900e75c76dc08440710e51f52f1aa6b451b429298896023e60db5b3139f", size = 69007, upload-time = "2026-04-24T13:15:15.662Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/fa/f9e3bd3c4d692b3ce9a2880a167d1f79681a1bea11f00d5bf76adc03e6ea/opentelemetry_exporter_otlp_proto_common-1.41.1.tar.gz", hash = "sha256:0e253156ea9c36b0bd3d2440c5c9ba7dd1f3fb64ba7a08fc85fbac536b56e1fb", size = 20409, upload-time = "2026-04-24T13:15:40.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/48/bce76d3ea772b609757e9bc844e02ab408a6446609bf74fb562062ba6b71/opentelemetry_exporter_otlp_proto_common-1.41.1-py3-none-any.whl", hash = "sha256:10da74dad6a49344b9b7b21b6182e3060373a235fde1528616d5f01f92e66aa9", size = 18366, upload-time = "2026-04-24T13:15:18.917Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/9b/e4503060b8695579dbaad187dc8cef4554188de68748c88060599b77489e/opentelemetry_exporter_otlp_proto_grpc-1.41.1.tar.gz", hash = "sha256:b05df8fa1333dc9a3fda36b676b96b5095ab6016d3f0c3296d430d629ba1443b", size = 25755, upload-time = "2026-04-24T13:15:41.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f2/c54f33c92443d087703e57e52e55f22f111373a5c4c4aa349ea60efe512e/opentelemetry_exporter_otlp_proto_grpc-1.41.1-py3-none-any.whl", hash = "sha256:537926dcef951136992479af1d9cd88f25e33d56c530e9f020ed57774dca2f94", size = 20297, upload-time = "2026-04-24T13:15:20.212Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.62b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/cb/0523b92c112a6cc70be43724343dc45225d3af134419844d7879a07755d4/opentelemetry_instrumentation-0.62b1.tar.gz", hash = "sha256:90e92a905ba4f84db06ac3aec96701df6c079b2d66e9379f8739f0a1bdcc7f45", size = 34043, upload-time = "2026-04-24T13:22:31.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/0f/45adbaea1f81b847cffdcee4f4b5f89297e42facf7fac78c7aaac4c38e75/opentelemetry_instrumentation-0.62b1-py3-none-any.whl", hash = "sha256:976fc6e640f2006599e97429c949e622c108d0c17c2059347d1e6c93c707f257", size = 34163, upload-time = "2026-04-24T13:21:31.722Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.62b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/43/b2f0703ff46718ff7b17d7fbf8e9d7f20e26a23c7c325092dd762d09cf9d/opentelemetry_instrumentation_asgi-0.62b1.tar.gz", hash = "sha256:7cf5f5d5c493bbb1edd2bd6d51fa879d964e94048904017258a32ffa47329310", size = 26781, upload-time = "2026-04-24T13:22:37.158Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/41/968c1fe12fb90abffca6620e65d4af91451c02ecca8f74a17a62cac490de/opentelemetry_instrumentation_asgi-0.62b1-py3-none-any.whl", hash = "sha256:b7f89be48528512619bd54fa2459f72afb1695ba71d7024d382ad96d467e7fa8", size = 17011, upload-time = "2026-04-24T13:21:38.006Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.62b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-asgi" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/38/91780475a25370b6d483afbaed3e1e170459d6351c5f7c08d66b65e2172e/opentelemetry_instrumentation_fastapi-0.62b1.tar.gz", hash = "sha256:b377d4ba32868fb1ff0f64da3fcdd3aa154d698fc83d65f5d380ea21bf31ee19", size = 25054, upload-time = "2026-04-24T13:22:50.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/6f/602e4081d3fe82731aff7e3e9c2f1662d85701841d6dc25f16a1874e11cd/opentelemetry_instrumentation_fastapi-0.62b1-py3-none-any.whl", hash = "sha256:93fa9cc4f315819aee5f4fceb6196c1e5b0fbd789c5520c631de228bd3e5285b", size = 13484, upload-time = "2026-04-24T13:21:54.538Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-httpx" +version = "0.62b1" +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/33/cb/7a418e69c7dad281803529cb4f6de1b747d802cca44c38032668690b4836/opentelemetry_instrumentation_httpx-0.62b1.tar.gz", hash = "sha256:a1fac9bcc3a6ef5996a7990563f1af0798468b2c146de535fd598369383fba7e", size = 24181, upload-time = "2026-04-24T13:22:52.124Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/e0/eca824e9492ccec00e055bdd243aeda8eb7c5eda746d98af4d7a2d97ecf3/opentelemetry_instrumentation_httpx-0.62b1-py3-none-any.whl", hash = "sha256:88614015df451d61bc7e73f22524e6f223611f80b6caad2f6bdcbe05fa0df653", size = 17201, upload-time = "2026-04-24T13:21:58.072Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-logging" +version = "0.62b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/25/a30e0160cb3654bb63936be16d8ffe5f4a658d10bec0d5509cca3c74f103/opentelemetry_instrumentation_logging-0.62b1.tar.gz", hash = "sha256:997359d29ce06cb3768677387469431d0484b2450b5c35d7f02361431d3de338", size = 18969, upload-time = "2026-04-24T13:22:54.275Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/e4/216d1c7ff9c10815a8587ecbca0b570596921f001d1e2c2903c6f19e2e90/opentelemetry_instrumentation_logging-0.62b1-py3-none-any.whl", hash = "sha256:969330216d1ae02f4e10f1a030566ae758114caead020817192e6a02c6d1a0e1", size = 17488, upload-time = "2026-04-24T13:22:00.726Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/e8/633c6d8a9c8840338b105907e55c32d3da1983abab5e52f899f72a82c3d1/opentelemetry_proto-1.41.1.tar.gz", hash = "sha256:4b9d2eb631237ea43b80e16c073af438554e32bc7e9e3f8ca4a9582f900020e5", size = 45670, upload-time = "2026-04-24T13:15:49.768Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/1e/5cd77035e3e82070e2265a63a760f715aacd3cb16dddc7efee913f297fcc/opentelemetry_proto-1.41.1-py3-none-any.whl", hash = "sha256:0496713b804d127a4147e32849fbaf5683fac8ee98550e8e7679cd706c289720", size = 72076, upload-time = "2026-04-24T13:15:32.542Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/d0/54ee30dab82fb0acda23d144502771ff76ef8728459c83c3e89ef9fb1825/opentelemetry_sdk-1.41.1.tar.gz", hash = "sha256:724b615e1215b5aeacda0abb8a6a8922c9a1853068948bd0bd225a56d0c792e6", size = 230180, upload-time = "2026-04-24T13:15:50.991Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/e7/a1420b698aad018e1cf60fdbaaccbe49021fb415e2a0d81c242f4c518f54/opentelemetry_sdk-1.41.1-py3-none-any.whl", hash = "sha256:edee379c126c1bce952b0c812b48fe8ff35b30df0eecf17e98afa4d598b7d85d", size = 180213, upload-time = "2026-04-24T13:15:33.767Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.62b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/de/911ac9e309052aca1b20b2d5549d3db45d1011e1a610e552c6ccdd1b64f8/opentelemetry_semantic_conventions-0.62b1.tar.gz", hash = "sha256:c5cc6e04a7f8c7cdd30be2ed81499fa4e75bfbd52c9cb70d40af1f9cd3619802", size = 145750, upload-time = "2026-04-24T13:15:52.236Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/a6/83dc2ab6fa397ee66fba04fe2e74bdf7be3b3870005359ceb7689103c058/opentelemetry_semantic_conventions-0.62b1-py3-none-any.whl", hash = "sha256:cf506938103d331fbb78eded0d9788095f7fd59016f2bda813c3324e5a74a93c", size = 231620, upload-time = "2026-04-24T13:15:35.454Z" }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.62b1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/1b/aa71b63e18d30a8384036b9937f40f7618f8030a7aa213155fb54f6f2b47/opentelemetry_util_http-0.62b1.tar.gz", hash = "sha256:adf6facbb89aef8f8bc566e2f04624942ba08a7b678b3479a91051a8f4dc70a3", size = 11393, upload-time = "2026-04-24T13:23:12.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/a9d9d32161c1ced61346267db4c9702da54f81ec5dc88214bc65c23f4e9d/opentelemetry_util_http-0.62b1-py3-none-any.whl", hash = "sha256:c57e8a6c19fc422c288e6074e882f506f85030b69b7376182f74f9257b9261f0", size = 9295, upload-time = "2026-04-24T13:22:28.078Z" }, +] + [[package]] name = "packaging" version = "26.2" @@ -623,6 +856,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, ] +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + [[package]] name = "pydantic" version = "2.13.3" @@ -1064,3 +1312,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, ] + +[[package]] +name = "zipp" +version = "3.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, +]