diff --git a/README.md b/README.md index 17bd54d..7d29c73 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,8 @@ OTel instrumentation is opt-in, controlled by environment variables: | Variable | Description | Default | | --------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------- | -| `OTEL_EXPORTER_OTLP_ENDPOINT` | Base OTLP endpoint (e.g. `http://collector:4318`). If unset, no OTel setup occurs. | _(disabled)_ | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | Base OTLP endpoint (e.g. `http://collector:4318` for HTTP, `http://collector:4317` for gRPC). If unset, no OTel setup occurs. | _(disabled)_ | +| `OTEL_EXPORTER_OTLP_PROTOCOL` | OTLP transport: `grpc` or `http/protobuf` (default). | `http/protobuf` | | `OTEL_SERVICE_NAME` | The `service.name` resource attribute. Defaults to `flagsmith-task-processor` when running the task processor. | `flagsmith-api` | | `OTEL_TRACING_EXCLUDED_URL_PATHS` | Comma-separated URL paths to exclude from tracing (e.g. `health/liveness,health/readiness`). | _(none)_ | @@ -143,7 +144,7 @@ Standard `OTEL_*` env vars (e.g. `OTEL_RESOURCE_ATTRIBUTES`, `OTEL_EXPORTER_OTLP When `OTEL_EXPORTER_OTLP_ENDPOINT` is set, `ensure_cli_env()` sets up: -- **Tracing**: `TracerProvider` with OTLP/HTTP span export, W3C `TraceContext` + `Baggage` propagation, and auto-instrumentation for: +- **Tracing**: `TracerProvider` with OTLP span export (HTTP or gRPC, per `OTEL_EXPORTER_OTLP_PROTOCOL`), W3C `TraceContext` + `Baggage` propagation, and auto-instrumentation for: - **Django** (`DjangoInstrumentor`): creates a root span per HTTP request with span names formatted as `{METHOD} {route_template}` (e.g. `GET /api/v1/projects/{pk}/`). - **psycopg2** (`Psycopg2Instrumentor`): creates child spans for each SQL query with `db.system`, `db.statement`, and `db.name` attributes. SQL commenter is enabled, adding trace context as SQL comments for database-side correlation. - **Redis** (`RedisInstrumentor`): creates child spans for each Redis command with `db.system` and `db.statement` attributes. diff --git a/pyproject.toml b/pyproject.toml index 89a2034..079964d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ optional-dependencies = { test-tools = [ "inflection", "opentelemetry-api (>=1.25,<2)", "opentelemetry-sdk (>=1.25,<2)", + "opentelemetry-exporter-otlp-proto-grpc (>=1.25,<2)", "opentelemetry-exporter-otlp-proto-http (>=1.25,<2)", "sentry-sdk (>=2.0.0,<3.0.0)", "structlog (>=24.4,<26)", diff --git a/src/common/core/constants.py b/src/common/core/constants.py index 9e41f99..f7626c0 100644 --- a/src/common/core/constants.py +++ b/src/common/core/constants.py @@ -1,3 +1,7 @@ +from common.core.types import OtlpProtocol + DEFAULT_PROMETHEUS_MULTIPROC_DIR_NAME = "flagsmith-prometheus" LOGGING_DEFAULT_ROOT_LOG_LEVEL = "WARNING" + +DEFAULT_OTLP_PROTOCOL: OtlpProtocol = "http/protobuf" diff --git a/src/common/core/main.py b/src/common/core/main.py index 7dec232..14c5f80 100644 --- a/src/common/core/main.py +++ b/src/common/core/main.py @@ -8,10 +8,12 @@ from django.core.management import ( execute_from_command_line as django_execute_from_command_line, ) -from environs import Env +from environs import Env, validate from common.core.cli import healthcheck, run +from common.core.constants import DEFAULT_OTLP_PROTOCOL from common.core.logging import setup_logging +from common.core.types import OtlpProtocol from common.gunicorn.processors import make_gunicorn_access_processor env = Env() @@ -63,6 +65,7 @@ def ensure_cli_env() -> typing.Generator[None, None, None]: build_otel_log_provider, build_tracer_provider, make_structlog_otel_processor, + resolve_otlp_export_endpoints, setup_tracing, ) @@ -72,17 +75,31 @@ def ensure_cli_env() -> typing.Generator[None, None, None]: else "flagsmith-api" ) service_name = env.str("OTEL_SERVICE_NAME", default_service_name) + otel_protocol = typing.cast( + OtlpProtocol, + env.str( + "OTEL_EXPORTER_OTLP_PROTOCOL", + DEFAULT_OTLP_PROTOCOL, + validate=validate.OneOf(typing.get_args(OtlpProtocol)), + ), + ) + traces_endpoint, logs_endpoint = resolve_otlp_export_endpoints( + otel_endpoint, + otel_protocol, + ) log_provider = build_otel_log_provider( - endpoint=f"{otel_endpoint}/v1/logs", + endpoint=logs_endpoint, service_name=service_name, + protocol=otel_protocol, ) otel_processors = [ add_otel_trace_context, make_structlog_otel_processor(log_provider), ] tracer_provider = build_tracer_provider( - endpoint=f"{otel_endpoint}/v1/traces", + endpoint=traces_endpoint, service_name=service_name, + protocol=otel_protocol, ) excluded_urls = env.str("OTEL_TRACING_EXCLUDED_URL_PATHS", None) ctx.enter_context(setup_tracing(tracer_provider, excluded_urls=excluded_urls)) diff --git a/src/common/core/otel.py b/src/common/core/otel.py index 414182e..15c2087 100644 --- a/src/common/core/otel.py +++ b/src/common/core/otel.py @@ -11,26 +11,37 @@ from opentelemetry import context as otel_context from opentelemetry._logs import SeverityNumber from opentelemetry.baggage.propagation import W3CBaggagePropagator +from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( + OTLPLogExporter as GrpcOTLPLogExporter, +) +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter as GrpcOTLPSpanExporter, +) from opentelemetry.exporter.otlp.proto.http._log_exporter import ( - OTLPLogExporter, + OTLPLogExporter as HttpOTLPLogExporter, ) from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( - OTLPSpanExporter, + OTLPSpanExporter as HttpOTLPSpanExporter, ) from opentelemetry.propagate import set_global_textmap from opentelemetry.propagators.composite import CompositePropagator from opentelemetry.propagators.textmap import TextMapPropagator from opentelemetry.sdk._logs import LoggerProvider -from opentelemetry.sdk._logs.export import BatchLogRecordProcessor +from opentelemetry.sdk._logs.export import ( + BatchLogRecordProcessor, + LogRecordExporter, +) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter from opentelemetry.trace.propagation.tracecontext import ( TraceContextTextMapPropagator, ) from opentelemetry.util.types import AnyValue, Attributes from structlog.typing import EventDict, Processor +from common.core.types import OtlpProtocol + _SEVERITY_MAP: dict[str, SeverityNumber] = { "debug": SeverityNumber.DEBUG, "info": SeverityNumber.INFO, @@ -51,6 +62,17 @@ ) +def resolve_otlp_export_endpoints( + base_endpoint: str, + protocol: OtlpProtocol, +) -> tuple[str, str]: + """Resolve trace and log exporter endpoints from a base OTLP endpoint.""" + normalised_base = base_endpoint.rstrip("/") + if protocol == "grpc": + return normalised_base, normalised_base + return f"{normalised_base}/v1/traces", f"{normalised_base}/v1/logs" + + def get_otel_event_name(*, logger_name: str | None, body: str) -> str: """Build the event name that reaches OTel from a structlog `(logger, event)`. @@ -153,20 +175,38 @@ def map_value_to_otel_value(value: object) -> str | int | float | bool: return json.dumps(value, default=str) -def build_otel_log_provider(*, endpoint: str, service_name: str) -> LoggerProvider: - """Create and configure an OTel LoggerProvider with OTLP/HTTP export.""" +def build_otel_log_provider( + *, + endpoint: str, + service_name: str, + protocol: OtlpProtocol, +) -> LoggerProvider: + """Create and configure an OTel LoggerProvider with OTLP export.""" resource = Resource.create({"service.name": service_name}) provider = LoggerProvider(resource=resource) - exporter = OTLPLogExporter(endpoint=endpoint) + exporter: LogRecordExporter + if protocol == "grpc": + exporter = GrpcOTLPLogExporter(endpoint=endpoint) + else: + exporter = HttpOTLPLogExporter(endpoint=endpoint) provider.add_log_record_processor(BatchLogRecordProcessor(exporter)) return provider -def build_tracer_provider(*, endpoint: str, service_name: str) -> TracerProvider: - """Create a TracerProvider with OTLP/HTTP export.""" +def build_tracer_provider( + *, + endpoint: str, + service_name: str, + protocol: OtlpProtocol, +) -> TracerProvider: + """Create a TracerProvider with OTLP export.""" resource = Resource.create({"service.name": service_name}) tracer_provider = TracerProvider(resource=resource) - span_exporter = OTLPSpanExporter(endpoint=endpoint) + span_exporter: SpanExporter + if protocol == "grpc": + span_exporter = GrpcOTLPSpanExporter(endpoint=endpoint) + else: + span_exporter = HttpOTLPSpanExporter(endpoint=endpoint) tracer_provider.add_span_processor(BatchSpanProcessor(span_exporter)) return tracer_provider diff --git a/src/common/core/types.py b/src/common/core/types.py new file mode 100644 index 0000000..85b85e1 --- /dev/null +++ b/src/common/core/types.py @@ -0,0 +1,3 @@ +from typing import Literal + +OtlpProtocol = Literal["grpc", "http/protobuf"] diff --git a/tests/unit/common/core/test_main.py b/tests/unit/common/core/test_main.py index 32b7862..0c13375 100644 --- a/tests/unit/common/core/test_main.py +++ b/tests/unit/common/core/test_main.py @@ -92,11 +92,13 @@ def test_ensure_cli_env__otel_endpoint_set__configures_otel_processors( mock_build_log.assert_called_once_with( endpoint="http://collector:4318/v1/logs", service_name="flagsmith-api", + protocol="http/protobuf", ) mock_make_processor.assert_called_once_with(mock_log_provider) mock_build_tracer.assert_called_once_with( endpoint="http://collector:4318/v1/traces", service_name="flagsmith-api", + protocol="http/protobuf", ) mock_setup_tracing.assert_called_once_with(mock_tracer_provider, excluded_urls=None) @@ -137,10 +139,12 @@ def test_ensure_cli_env__otel_custom_service_name_and_excluded_urls__passes_to_p mock_build_log.assert_called_once_with( endpoint="http://collector:4318/v1/logs", service_name="my-service", + protocol="http/protobuf", ) mock_build_tracer.assert_called_once_with( endpoint="http://collector:4318/v1/traces", service_name="my-service", + protocol="http/protobuf", ) mock_setup_tracing.assert_called_once_with( mock_build_tracer.return_value, @@ -213,10 +217,12 @@ def test_ensure_cli_env__argv__expected_otel_service_name( mock_build_log.assert_called_once_with( endpoint="http://collector:4318/v1/logs", service_name=expected_otel_service_name, + protocol="http/protobuf", ) mock_build_tracer.assert_called_once_with( endpoint="http://collector:4318/v1/traces", service_name=expected_otel_service_name, + protocol="http/protobuf", ) @@ -247,10 +253,47 @@ def test_ensure_cli_env__env_service_name__expected_otel_service_name( mock_build_log.assert_called_once_with( endpoint="http://collector:4318/v1/logs", service_name="my-custom", + protocol="http/protobuf", ) mock_build_tracer.assert_called_once_with( endpoint="http://collector:4318/v1/traces", service_name="my-custom", + protocol="http/protobuf", + ) + + +def test_ensure_cli_env__otel_grpc_protocol__uses_grpc_endpoints( + monkeypatch: pytest.MonkeyPatch, + mocker: MockerFixture, +) -> None: + # Given + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4317") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc") + + mock_build_log = mocker.patch( + "common.core.otel.build_otel_log_provider", + return_value=mocker.MagicMock(spec=LoggerProvider), + ) + mock_build_tracer = mocker.patch( + "common.core.otel.build_tracer_provider", + return_value=mocker.MagicMock(spec=TracerProvider), + ) + mocker.patch("common.core.otel.setup_tracing") + + # When + with ensure_cli_env(): + pass + + # Then + mock_build_log.assert_called_once_with( + endpoint="http://collector:4317", + service_name="flagsmith-api", + protocol="grpc", + ) + mock_build_tracer.assert_called_once_with( + endpoint="http://collector:4317", + service_name="flagsmith-api", + protocol="grpc", ) diff --git a/tests/unit/common/core/test_otel.py b/tests/unit/common/core/test_otel.py index afd8a0f..0877a70 100644 --- a/tests/unit/common/core/test_otel.py +++ b/tests/unit/common/core/test_otel.py @@ -16,6 +16,7 @@ build_otel_log_provider, build_tracer_provider, make_structlog_otel_processor, + resolve_otlp_export_endpoints, setup_tracing, ) @@ -187,6 +188,7 @@ def test_build_otel_log_provider__valid_args__returns_configured_provider() -> N provider = build_otel_log_provider( endpoint="http://localhost:4318/v1/logs", service_name="test-service", + protocol="http/protobuf", ) # Then @@ -194,11 +196,30 @@ def test_build_otel_log_provider__valid_args__returns_configured_provider() -> N assert provider.resource.attributes["service.name"] == "test-service" +def test_build_otel_log_provider__grpc_protocol__uses_grpc_exporter( + mocker: pytest_mock.MockerFixture, +) -> None: + # Given + mock_grpc_exporter = mocker.patch("common.core.otel.GrpcOTLPLogExporter") + + # When + provider = build_otel_log_provider( + endpoint="http://localhost:4317", + service_name="test-service", + protocol="grpc", + ) + + # Then + assert isinstance(provider, LoggerProvider) + mock_grpc_exporter.assert_called_once_with(endpoint="http://localhost:4317") + + def test_build_tracer_provider__valid_args__returns_configured_provider() -> None: # Given / When provider = build_tracer_provider( endpoint="http://localhost:4318/v1/traces", service_name="test-service", + protocol="http/protobuf", ) # Then @@ -206,6 +227,64 @@ def test_build_tracer_provider__valid_args__returns_configured_provider() -> Non assert provider.resource.attributes["service.name"] == "test-service" +def test_build_tracer_provider__grpc_protocol__uses_grpc_exporter( + mocker: pytest_mock.MockerFixture, +) -> None: + # Given + mock_grpc_exporter = mocker.patch("common.core.otel.GrpcOTLPSpanExporter") + + # When + provider = build_tracer_provider( + endpoint="http://localhost:4317", + service_name="test-service", + protocol="grpc", + ) + + # Then + assert isinstance(provider, TracerProvider) + mock_grpc_exporter.assert_called_once_with(endpoint="http://localhost:4317") + + +@pytest.mark.parametrize( + "base_endpoint, protocol, expected_traces, expected_logs", + [ + ( + "http://collector:4318", + "http/protobuf", + "http://collector:4318/v1/traces", + "http://collector:4318/v1/logs", + ), + ( + "http://collector:4318/", + "http/protobuf", + "http://collector:4318/v1/traces", + "http://collector:4318/v1/logs", + ), + ( + "http://collector:4317", + "grpc", + "http://collector:4317", + "http://collector:4317", + ), + ], +) +def test_resolve_otlp_export_endpoints__protocol__returns_expected_urls( + base_endpoint: str, + protocol: str, + expected_traces: str, + expected_logs: str, +) -> None: + # Given / When + traces_endpoint, logs_endpoint = resolve_otlp_export_endpoints( + base_endpoint, + protocol, # type: ignore[arg-type] + ) + + # Then + assert traces_endpoint == expected_traces + assert logs_endpoint == expected_logs + + def test_setup_tracing__called__instruments_and_uninstruments_all_libraries( mocker: pytest_mock.MockerFixture, ) -> None: diff --git a/uv.lock b/uv.lock index e95e2f2..3ecb207 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,11 @@ version = 1 revision = 3 requires-python = ">=3.11, <4.0" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version < '3.13'", +] [[package]] name = "annotated-types" @@ -463,6 +468,7 @@ common-core = [ { name = "gunicorn" }, { name = "inflection" }, { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-http" }, { name = "opentelemetry-instrumentation-django" }, { name = "opentelemetry-instrumentation-psycopg2" }, @@ -485,6 +491,7 @@ flagsmith-schemas = [ otel = [ { name = "inflection" }, { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-http" }, { name = "opentelemetry-sdk" }, { name = "sentry-sdk" }, @@ -545,6 +552,7 @@ requires-dist = [ { name = "inflection", marker = "extra == 'otel'" }, { name = "opentelemetry-api", marker = "extra == 'otel'", specifier = ">=1.25,<2" }, { name = "opentelemetry-api", marker = "extra == 'task-processor'", specifier = ">=1.25,<2" }, + { name = "opentelemetry-exporter-otlp-proto-grpc", marker = "extra == 'otel'", specifier = ">=1.25,<2" }, { name = "opentelemetry-exporter-otlp-proto-http", marker = "extra == 'otel'", specifier = ">=1.25,<2" }, { name = "opentelemetry-instrumentation-django", marker = "extra == 'common-core'", specifier = ">=0.46b0,<1" }, { name = "opentelemetry-instrumentation-psycopg2", marker = "extra == 'common-core'", specifier = ">=0.46b0,<1" }, @@ -626,6 +634,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/82/fcb6520612bec0c39b973a6c0954b6a0d948aadfe8f7e9487f60ceb8bfa6/googleapis_common_protos-1.73.1-py3-none-any.whl", hash = "sha256:e51f09eb0a43a8602f5a915870972e6b4a394088415c79d79605a46d8e826ee8", size = 297556, upload-time = "2026-03-26T22:15:58.455Z" }, ] +[[package]] +name = "grpcio" +version = "1.81.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/b5/1ff353970a87eda4c98251e34d2dfd214abd4982dc89119c9252a2a482d2/grpcio-1.81.1.tar.gz", hash = "sha256:6fa10a767143a5e82e8eaab53918af0cd8909a57a27f8cb2288b80a613ac671b", size = 13026582, upload-time = "2026-06-11T12:46:51.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/ea/1c2fa386b718ff493225e61cfc052ef400b4d6ffc54cbe261026432624b5/grpcio-1.81.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:d71d30f2d92f67d944631c523713934fee37292469e182ebcd2c1dd8a64ce53f", size = 6093112, upload-time = "2026-06-11T12:44:52.131Z" }, + { url = "https://files.pythonhosted.org/packages/2b/18/acf45fa8bd1bc5d7b0c2fd3dc4c209379fbd5bb396b440b68a83342226b7/grpcio-1.81.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b137f4bf3ada9dc44d411478decc6ff09a79ed30b306cd2abaa98408c3588137", size = 12074277, upload-time = "2026-06-11T12:44:55.354Z" }, + { url = "https://files.pythonhosted.org/packages/48/d7/ee86a60699b7db039f772a2c4a7e4facc7138984ff42c0130933a0063884/grpcio-1.81.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a3acb384427816dd5d470f47e62137b87f74da694faa8a50147012cf40df276a", size = 6640348, upload-time = "2026-06-11T12:44:59.223Z" }, + { url = "https://files.pythonhosted.org/packages/26/ee/d2de5e47378ffc207d476c230fea3be4d2601edbce9995f4fe45535d4896/grpcio-1.81.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f9a0ebbe45c29b5e5866593c12b78bd9035f0f0f0d4bc8361680cd580d99db49", size = 7331842, upload-time = "2026-06-11T12:45:02.001Z" }, + { url = "https://files.pythonhosted.org/packages/23/d6/abeda5c2b896a0b341584fe5ac411bbf72e197a9a374c355fb90965e08d2/grpcio-1.81.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a37165cc80b1a368384b383e63a4c38116a10467ae44c904d2d7468c4470ec2", size = 6842229, upload-time = "2026-06-11T12:45:04.76Z" }, + { url = "https://files.pythonhosted.org/packages/10/1c/1f0da7d590b4aeee006826ba568d0e419ca14b23e18f901a3da3e9fba613/grpcio-1.81.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6282caffb41ec326d4cb67ca9cf53b739d1b2f975a2acb498c7418e9f7d9a416", size = 7446096, upload-time = "2026-06-11T12:45:07.499Z" }, + { url = "https://files.pythonhosted.org/packages/6a/81/5c505d508f7c887aa7982d21443a4126597c80d34b0bcf40f9cec576d7f3/grpcio-1.81.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a35009284d0d3d5c2c9601c164a911b8b4331608d98a9a66d47d97bb2f522b70", size = 8445238, upload-time = "2026-06-11T12:45:10.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b2/524847365122ee509ca17bcc4e092198b700e94af7bfd5bb5e6dd9f3ee66/grpcio-1.81.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1b22c80559854b789a01fd89e8929b3798a156c0829b5282a8939f33ad4115ad", size = 7873989, upload-time = "2026-06-11T12:45:13.102Z" }, + { url = "https://files.pythonhosted.org/packages/18/fa/07c037c50b006909d1d13a5848774f8aa7b242f70dc03a035c64eea0e6db/grpcio-1.81.1-cp311-cp311-win32.whl", hash = "sha256:428bec0161b48d8cf583c068591bc0016d0d9cfff52462b72b3884861ea768c5", size = 4202223, upload-time = "2026-06-11T12:45:16.166Z" }, + { url = "https://files.pythonhosted.org/packages/41/ed/6bff15376920942fac6b95b9802752b837437172c9e8fc2d3170546b89cc/grpcio-1.81.1-cp311-cp311-win_amd64.whl", hash = "sha256:30e825f6848d9f18bba350ed6c75c1b02a0b5184474a31db9a32b1fa66fd8c79", size = 4941303, upload-time = "2026-06-11T12:45:18.724Z" }, + { url = "https://files.pythonhosted.org/packages/85/07/9a979c81738863a738dc23d65177056e71fbb2db817740ed870b33434e7a/grpcio-1.81.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:8b39472beafc0bdcafc4c8c73ad082ebfdb449d566897a61e7acb4fa88089115", size = 6053264, upload-time = "2026-06-11T12:45:21.017Z" }, + { url = "https://files.pythonhosted.org/packages/75/95/539706ca0d3bd40dbad583dc56fd883da941f37556b629132da5762781b9/grpcio-1.81.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:12b7524c88d4026d3dcb7b0ebe16b6714f3b4af402ddd0f0639ab064a00c87c3", size = 12052560, upload-time = "2026-06-11T12:45:23.652Z" }, + { url = "https://files.pythonhosted.org/packages/e0/44/f257b7e0bd69c93b06c6cb8ac8d1b901ccb42bedabd83c1a4c77a71f8810/grpcio-1.81.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1e123f9b37edb8375fd74130d1f69c944bbf0a7b06761ae7211154b8759e94d2", size = 6595983, upload-time = "2026-06-11T12:45:26.963Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f3/19782aa04c960968bef8c5539329d8e3bbc3364e2e46d19eb5e5cc5e43b7/grpcio-1.81.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2c2e2ae6867c2966b8daccc836d54a13218e0007e9a490aeb81dd05be64d22d7", size = 7303455, upload-time = "2026-06-11T12:45:29.707Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8c/dea020b6d91508cd84463917a63149ec196ee7db505d032ae43fcb3303b9/grpcio-1.81.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:766bc7c9a9c340342f4c864ccbda8e78111e4751f13b895812b9c148fb79e9d0", size = 6809167, upload-time = "2026-06-11T12:45:32.52Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/3030dd940408083bd32cd95d634777a71605ade4887154d93e8a89244946/grpcio-1.81.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b259a04a737cb3496be0901328eb8b7552ed8df4865d8c8f1cf1bffcfc0776a3", size = 7412536, upload-time = "2026-06-11T12:45:35.403Z" }, + { url = "https://files.pythonhosted.org/packages/e0/dd/1172a9e42b168edcafefad6115346ef619a3fc02158bb170e66ced24bcdd/grpcio-1.81.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:85b10a45b8993d195c4f3ff57025b8d1e11834909ee475c403bfa60cb4caefaf", size = 8408276, upload-time = "2026-06-11T12:45:37.78Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/71437c7f3596e5246155c515852795a85a1a8d228190212432b13b97a95d/grpcio-1.81.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8ea1936c26b99999b27479853039a7f34713f56c49375ad52b38535ec93a796c", size = 7849660, upload-time = "2026-06-11T12:45:40.627Z" }, + { url = "https://files.pythonhosted.org/packages/65/40/7debc0da45d2efebafb82da75644be347497fe4ee250514b8cd3b86ae8bf/grpcio-1.81.1-cp312-cp312-win32.whl", hash = "sha256:a185a04039df6cae8648bc8ab6d6fde7bf94f7188ecf7828e76ac52eef1e41d6", size = 4185819, upload-time = "2026-06-11T12:45:43.027Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b9/8fe3ba5ed462067774ebc1f9c7f26aa7ebcc280ddd476be107153de1339e/grpcio-1.81.1-cp312-cp312-win_amd64.whl", hash = "sha256:3ad74f8bb1a18963914c5452d289422830b39459e8776ebbcd207be1fbfb1d94", size = 4930461, upload-time = "2026-06-11T12:45:45.775Z" }, + { url = "https://files.pythonhosted.org/packages/7a/42/dcc2e4b600538ef18327c0839d56b7d3c3812337c5d710df5877dbb39b1e/grpcio-1.81.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:b10e1ff4756ed27d5a29d7fc79cfce7ef1ff56ad20025b89bac7cf79e09abbbe", size = 6054466, upload-time = "2026-06-11T12:45:48.43Z" }, + { url = "https://files.pythonhosted.org/packages/7b/4a/a36e03210183a8a7d4c80c3936acee679f4bd77d5861f369db47b2cc5f05/grpcio-1.81.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:819edbdcb42ab8598b494bcf0222684bbb7a3c772bd1b1f0be7e029a6063c28e", size = 12048795, upload-time = "2026-06-11T12:45:54.011Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d5/d68e30b29098f63beab6fe501100fe82674ff142b32c672532da86a99b3a/grpcio-1.81.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5bf2dc311127d91230cc79b92188c082634a06cf66c5234db49a43b910183b0", size = 6599094, upload-time = "2026-06-11T12:45:57.799Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b3/e837954d279754f638a11cca5dcf6b24a005efb398984cefaf7735945a54/grpcio-1.81.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e8ca6a1fcdb2943c9cbc1804a1baf3acb6071d72a471591678ded84218006e14", size = 7307182, upload-time = "2026-06-11T12:46:00.568Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/b47957057e729adc6cdf519a47f8be2562b7140e280f1418443eb4022192/grpcio-1.81.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e64dd101d380a115cc5a0c7856788adb535f1a4e21fc543775602f8be95180ae", size = 6810962, upload-time = "2026-06-11T12:46:03.312Z" }, + { url = "https://files.pythonhosted.org/packages/40/26/569868e364e05b19ec8f969da53d230bcd89c962cd198f7c29943155c4d3/grpcio-1.81.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:98a07f9bf591e3a8919797bee1c53f026ba4acd587e5a4404c8e57c9ec36b2a5", size = 7415698, upload-time = "2026-06-11T12:46:06.005Z" }, + { url = "https://files.pythonhosted.org/packages/36/0c/5440a0582cb5653fc42a6e262eeb22700943313f8076f9dc927491b20a59/grpcio-1.81.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c261d74b1a945cf895a9d6eccd1685a8e837531beaab782da4d630a8d12deffb", size = 8407779, upload-time = "2026-06-11T12:46:08.84Z" }, + { url = "https://files.pythonhosted.org/packages/ff/aa/66fe9f39871d766987d869a03ee0842a026f499c7b1e62decb9e78a8088e/grpcio-1.81.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58ad1131c300d3c9b933802b3cc4dc69d380822935ba50b28703156ea826fbf7", size = 7844521, upload-time = "2026-06-11T12:46:12.171Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9e/69bb7194861bcd28fb3193261d4f9c3831b4446993f002cf59068943e7ab/grpcio-1.81.1-cp313-cp313-win32.whl", hash = "sha256:78e29211f26da2fdd0e9c6d2b79f489476140cf7029b6a64808ade7ca4156a42", size = 4182786, upload-time = "2026-06-11T12:46:15.192Z" }, + { url = "https://files.pythonhosted.org/packages/0d/20/3da8bb0d637feccdc3e1e419bb511ce93651ce7d54164f95de22cc0b8b34/grpcio-1.81.1-cp313-cp313-win_amd64.whl", hash = "sha256:edb59506291b647a30884b1d51a599d605f40b20af4a7dc3d33786a47a31de60", size = 4928648, upload-time = "2026-06-11T12:46:17.823Z" }, + { url = "https://files.pythonhosted.org/packages/b6/58/19414622b1bf6981bc9c05a365bd548e71876c89000083b3af489251e9c0/grpcio-1.81.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:506f48f2f9c29b143fca3dad7b0d518c188b6c9648c75a2ae6e2d9f2c13a060b", size = 6055336, upload-time = "2026-06-11T12:46:20.557Z" }, + { url = "https://files.pythonhosted.org/packages/32/f1/2ec88adb92b0eba970dd0e0e7dd086341daa3c75eba4f735f9e44bf684b0/grpcio-1.81.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d865db4a6318e1c1bea83292e0ed231090538fc4ca45425b0f0480eb338bbc6e", size = 12056279, upload-time = "2026-06-11T12:46:24.255Z" }, + { url = "https://files.pythonhosted.org/packages/41/36/e8c5f8c6ec71de73733695ebc809e98b178b534ec6d8eaa31a7ebab4ad4c/grpcio-1.81.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2aa72e3ce1770317ef534f63d397b55e130725f5149bd36077c3b539019db27", size = 6608225, upload-time = "2026-06-11T12:46:27.601Z" }, + { url = "https://files.pythonhosted.org/packages/30/22/96fc577a845ab093326d9ab1adb874bd4936c8cf98ac8ed2f3db13a0a2fb/grpcio-1.81.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0490c30c261eded63f3f354979f9dc4502a9fb944cccb60cd9dc85f5a7349854", size = 7306576, upload-time = "2026-06-11T12:46:30.514Z" }, + { url = "https://files.pythonhosted.org/packages/76/7b/61dab5d5969f28d97fb1009cead1df0a5cd987d3315e1b37f18a4449f8bc/grpcio-1.81.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:410482da976329fe5f4067270401b12cf2bd552ff8020f054ecfaddb5475f9d6", size = 6812165, upload-time = "2026-06-11T12:46:33.699Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/6e501929d4f5f96462fd82fd9f0f06e5f9612207582b862868d68757b27d/grpcio-1.81.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3657301562ac3cb8018d30d0d3ebfa39932239f7b5703422057ef14b69949f5", size = 7422962, upload-time = "2026-06-11T12:46:36.511Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7e/f2157589e66daa78ebb3165942d05a08bdea93b9d11c2bc1e172aef89685/grpcio-1.81.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:24c8e57504c8f45b237e40b99262d181071e5099a07053695b75d97bb53053a0", size = 8408176, upload-time = "2026-06-11T12:46:39.803Z" }, + { url = "https://files.pythonhosted.org/packages/da/df/c6717fef716e00d235ffb96123baf6dce76d6004f6233fa767c502861460/grpcio-1.81.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b427c19380991a4eaab2f6144b64b99b412043314c6bf4ab544f97bb31ee4190", size = 7846681, upload-time = "2026-06-11T12:46:43.013Z" }, + { url = "https://files.pythonhosted.org/packages/36/84/3502e9f210a6a5c4438c8aca3f88edd2e04f6a27f3d41b26cf0a0024b096/grpcio-1.81.1-cp314-cp314-win32.whl", hash = "sha256:61233fe8951e5c85dff81c2458b6528624760166946b5b47ea150a589168411f", size = 4264615, upload-time = "2026-06-11T12:46:45.741Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/4af731ff7492c68a96e4c71bfd0f4590acde92b31c6fe4894e6465c10ff6/grpcio-1.81.1-cp314-cp314-win_amd64.whl", hash = "sha256:3768a5ff1b2125e6f552e561b6b2dca0e64982d8949689b4df145cf8b98d7821", size = 5070275, upload-time = "2026-06-11T12:46:48.486Z" }, +] + [[package]] name = "gunicorn" version = "23.0.0" @@ -988,6 +1047,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8b/ca/8f122055c97a932311a3f640273f084e738008933503d0c2563cd5d591fc/opentelemetry_exporter_otlp_proto_common-1.40.0-py3-none-any.whl", hash = "sha256:7081ff453835a82417bf38dccf122c827c3cbc94f2079b03bba02a3165f25149", size = 18369, upload-time = "2026-03-04T14:17:04.796Z" }, ] +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.40.0" +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/8f/7f/b9e60435cfcc7590fa87436edad6822240dddbc184643a2a005301cc31f4/opentelemetry_exporter_otlp_proto_grpc-1.40.0.tar.gz", hash = "sha256:bd4015183e40b635b3dab8da528b27161ba83bf4ef545776b196f0fb4ec47740", size = 25759, upload-time = "2026-03-04T14:17:24.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/6f/7ee0980afcbdcd2d40362da16f7f9796bd083bf7f0b8e038abfbc0300f5d/opentelemetry_exporter_otlp_proto_grpc-1.40.0-py3-none-any.whl", hash = "sha256:2aa0ca53483fe0cf6405087a7491472b70335bc5c7944378a0a8e72e86995c52", size = 20304, upload-time = "2026-03-04T14:17:05.942Z" }, +] + [[package]] name = "opentelemetry-exporter-otlp-proto-http" version = "1.40.0"