Trace Postgres and Redis queries with the contrib instrumentations
CI / Build and push docker image (push) Successful in 2m42s
CI / Build and push docker image (push) Successful in 2m42s
Builds the TracerProvider in tavolo.telemetry (kaya-otel's is private) and injects it into kaya-otel, then instruments asyncpg and redis through opentelemetry-instrumentation-asyncpg/redis. Redis command values are redacted and asyncpg spans carry the parameterized SQL only.
This commit is contained in:
@@ -71,9 +71,9 @@ data:
|
|||||||
# CORS_ALLOW_CREDENTIALS: "false"
|
# CORS_ALLOW_CREDENTIALS: "false"
|
||||||
# CORS_EXPOSE_HEADERS: ""
|
# CORS_EXPOSE_HEADERS: ""
|
||||||
# CORS_MAX_AGE: "600"
|
# CORS_MAX_AGE: "600"
|
||||||
# OpenTelemetry (kaya-otel + system-metrics): traces, metrics and logs
|
# OpenTelemetry (kaya-otel + contrib instrumentations): traces
|
||||||
# via OTLP/HTTP, disabled unless OTEL_ENABLED is set. The image bundles
|
# (HTTP/WebSocket, Postgres, Redis), metrics and logs via OTLP/HTTP,
|
||||||
# the otel extra.
|
# disabled unless OTEL_ENABLED is set. The image bundles the otel extra.
|
||||||
# OTEL_ENABLED: "true"
|
# OTEL_ENABLED: "true"
|
||||||
# OTEL_SERVICE_NAME: "tavolo"
|
# OTEL_SERVICE_NAME: "tavolo"
|
||||||
# OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.observability:4318"
|
# OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.observability:4318"
|
||||||
|
|||||||
+3
-2
@@ -122,8 +122,9 @@ services:
|
|||||||
CORS_ALLOW_CREDENTIALS: ${CORS_ALLOW_CREDENTIALS:-}
|
CORS_ALLOW_CREDENTIALS: ${CORS_ALLOW_CREDENTIALS:-}
|
||||||
CORS_EXPOSE_HEADERS: ${CORS_EXPOSE_HEADERS:-}
|
CORS_EXPOSE_HEADERS: ${CORS_EXPOSE_HEADERS:-}
|
||||||
CORS_MAX_AGE: ${CORS_MAX_AGE:-}
|
CORS_MAX_AGE: ${CORS_MAX_AGE:-}
|
||||||
# OpenTelemetry (kaya-otel + system-metrics): traces, metrics and
|
# OpenTelemetry (kaya-otel + contrib instrumentations): traces
|
||||||
# logs via OTLP/HTTP, disabled unless OTEL_ENABLED is set.
|
# (HTTP/WebSocket, Postgres, Redis), metrics and logs via OTLP/HTTP,
|
||||||
|
# disabled unless OTEL_ENABLED is set.
|
||||||
# The image bundles the otel extra (see packages/tavolo-app/pyproject.toml
|
# The image bundles the otel extra (see packages/tavolo-app/pyproject.toml
|
||||||
# and server/requirements.txt).
|
# and server/requirements.txt).
|
||||||
OTEL_ENABLED: ${OTEL_ENABLED:-}
|
OTEL_ENABLED: ${OTEL_ENABLED:-}
|
||||||
|
|||||||
+6
-5
@@ -61,11 +61,12 @@ TURN_TIMEOUT_SECONDS=30
|
|||||||
# Seconds browsers may cache the preflight response (default 600):
|
# Seconds browsers may cache the preflight response (default 600):
|
||||||
#CORS_MAX_AGE=600
|
#CORS_MAX_AGE=600
|
||||||
|
|
||||||
# OpenTelemetry (via kaya-otel + the contrib system-metrics package). Off
|
# OpenTelemetry (via kaya-otel + the contrib instrumentation packages).
|
||||||
# unless OTEL_ENABLED is truthy; exports traces, HTTP/app/process metrics
|
# Off unless OTEL_ENABLED is truthy; exports traces (HTTP/WebSocket,
|
||||||
# and logs over OTLP/HTTP (requires the tavolo-app "otel" extra, bundled
|
# Postgres via asyncpg, Redis), metrics and logs over OTLP/HTTP (requires
|
||||||
# in the Docker image). The endpoint defaults to the standard
|
# the tavolo-app "otel" extra, bundled in the Docker image). The endpoint
|
||||||
# OTEL_EXPORTER_OTLP_ENDPOINT (localhost:4318) when unset.
|
# defaults to the standard OTEL_EXPORTER_OTLP_ENDPOINT (localhost:4318)
|
||||||
|
# when unset.
|
||||||
#OTEL_ENABLED=true
|
#OTEL_ENABLED=true
|
||||||
#OTEL_SERVICE_NAME=tavolo
|
#OTEL_SERVICE_NAME=tavolo
|
||||||
#OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
|
#OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
|
||||||
|
|||||||
+5
-1
@@ -130,7 +130,11 @@ loggers:
|
|||||||
With `OTEL_ENABLED` set, the app exports over OTLP/HTTP (kaya-otel plus
|
With `OTEL_ENABLED` set, the app exports over OTLP/HTTP (kaya-otel plus
|
||||||
`tavolo.telemetry`):
|
`tavolo.telemetry`):
|
||||||
|
|
||||||
- **Traces**: HTTP server spans and one span per WebSocket connection.
|
- **Traces**: HTTP server spans, one span per WebSocket connection, plus
|
||||||
|
database spans for the Postgres (asyncpg, through Tortoise) and Redis
|
||||||
|
clients. Redis command values are redacted by the instrumentation
|
||||||
|
(`GET ? ?`) and asyncpg spans carry the parameterized SQL only — no
|
||||||
|
parameter values.
|
||||||
- **Metrics**: the HTTP server metrics from kaya-otel, plus
|
- **Metrics**: the HTTP server metrics from kaya-otel, plus
|
||||||
`tavolo.active_users` (sessions in the session store),
|
`tavolo.active_users` (sessions in the session store),
|
||||||
`tavolo.ongoing_games` (live matches not finished yet, maintained by the
|
`tavolo.ongoing_games` (live matches not finished yet, maintained by the
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ The deployable tavolo application: the composition root wiring
|
|||||||
`tavolo.app:app` for granian.
|
`tavolo.app:app` for granian.
|
||||||
- `config.py` — environment-driven frozen `Settings`.
|
- `config.py` — environment-driven frozen `Settings`.
|
||||||
- `telemetry.py` — the optional OpenTelemetry wiring beyond kaya-otel:
|
- `telemetry.py` — the optional OpenTelemetry wiring beyond kaya-otel:
|
||||||
meter/logger providers, the `tavolo.active_users`/`tavolo.ongoing_games`
|
meter/tracer/logger providers, the
|
||||||
gauges (polled from the stores), process/CPython metrics via
|
`tavolo.active_users`/`tavolo.ongoing_games` gauges (polled from the
|
||||||
`opentelemetry-instrumentation-system-metrics` and OTLP log export.
|
stores), process/CPython metrics via
|
||||||
|
`opentelemetry-instrumentation-system-metrics`, Postgres/Redis query
|
||||||
|
spans via the asyncpg/redis instrumentations, and OTLP log export.
|
||||||
- `aerich_config.py` — Tortoise ORM configuration consumed by the aerich
|
- `aerich_config.py` — Tortoise ORM configuration consumed by the aerich
|
||||||
CLI (migrations live in `server/migrations/`; aerich itself is
|
CLI (migrations live in `server/migrations/`; aerich itself is
|
||||||
configured via the `[tool.aerich]` section of `server/pyproject.toml`).
|
configured via the `[tool.aerich]` section of `server/pyproject.toml`).
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ dev = [
|
|||||||
]
|
]
|
||||||
otel = [
|
otel = [
|
||||||
"kaya-otel>=0.0.4",
|
"kaya-otel>=0.0.4",
|
||||||
|
"opentelemetry-instrumentation-asyncpg>=0.66b0",
|
||||||
|
"opentelemetry-instrumentation-redis>=0.66b0",
|
||||||
"opentelemetry-instrumentation-system-metrics>=0.66b0",
|
"opentelemetry-instrumentation-system-metrics>=0.66b0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,9 @@ def cors_mixin_from_settings(settings: Settings) -> Optional[CorsMixin]:
|
|||||||
|
|
||||||
|
|
||||||
def otel_mixin_from_settings(
|
def otel_mixin_from_settings(
|
||||||
settings: Settings, meter_provider: Optional[Any] = None
|
settings: Settings,
|
||||||
|
meter_provider: Optional[Any] = None,
|
||||||
|
tracer_provider: Optional[Any] = None,
|
||||||
) -> Optional[KayaMixin]:
|
) -> Optional[KayaMixin]:
|
||||||
"""Build a :class:`~kaya.otel.OTelMixin` from the OTEL_* settings.
|
"""Build a :class:`~kaya.otel.OTelMixin` from the OTEL_* settings.
|
||||||
|
|
||||||
@@ -89,10 +91,11 @@ def otel_mixin_from_settings(
|
|||||||
is imported lazily here: default installs and the test suite never need
|
is imported lazily here: default installs and the test suite never need
|
||||||
the OpenTelemetry packages.
|
the OpenTelemetry packages.
|
||||||
|
|
||||||
``meter_provider`` is the provider built by
|
``meter_provider``/``tracer_provider`` are the providers built by
|
||||||
:func:`tavolo.telemetry.telemetry_from_settings`; injecting it lets the
|
:func:`tavolo.telemetry.telemetry_from_settings`; injecting them lets
|
||||||
custom gauges share the same OTLP pipeline (kaya-otel then leaves its
|
the custom instruments and the database instrumentations share the same
|
||||||
shutdown to the telemetry mixin).
|
OTLP pipelines (kaya-otel then leaves their shutdown to the telemetry
|
||||||
|
mixin).
|
||||||
"""
|
"""
|
||||||
if not settings.otel_enabled:
|
if not settings.otel_enabled:
|
||||||
return None
|
return None
|
||||||
@@ -111,6 +114,8 @@ def otel_mixin_from_settings(
|
|||||||
}
|
}
|
||||||
if meter_provider is not None:
|
if meter_provider is not None:
|
||||||
kwargs["meter_provider"] = meter_provider
|
kwargs["meter_provider"] = meter_provider
|
||||||
|
if tracer_provider is not None:
|
||||||
|
kwargs["tracer_provider"] = tracer_provider
|
||||||
return OTelMixin(**kwargs)
|
return OTelMixin(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
@@ -190,7 +195,9 @@ mixins: list[KayaMixin] = [session_mixin, oidc_mixin, tortoise_mixin, openapi_mi
|
|||||||
telemetry = telemetry_from_settings(settings, session_store, game_store)
|
telemetry = telemetry_from_settings(settings, session_store, game_store)
|
||||||
if telemetry is not None:
|
if telemetry is not None:
|
||||||
otel_mixin = otel_mixin_from_settings(
|
otel_mixin = otel_mixin_from_settings(
|
||||||
settings, meter_provider=telemetry.meter_provider
|
settings,
|
||||||
|
meter_provider=telemetry.meter_provider,
|
||||||
|
tracer_provider=telemetry.tracer_provider,
|
||||||
)
|
)
|
||||||
assert otel_mixin is not None # OTEL_ENABLED was checked above
|
assert otel_mixin is not None # OTEL_ENABLED was checked above
|
||||||
# kaya-otel first: before hooks run in registration order (after hooks
|
# kaya-otel first: before hooks run in registration order (after hooks
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ from .config import Settings
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from opentelemetry.sdk._logs import LoggerProvider
|
from opentelemetry.sdk._logs import LoggerProvider
|
||||||
from opentelemetry.sdk.metrics import MeterProvider
|
from opentelemetry.sdk.metrics import MeterProvider
|
||||||
|
from opentelemetry.sdk.trace import TracerProvider
|
||||||
|
|
||||||
log = getLogger(__name__)
|
log = getLogger(__name__)
|
||||||
|
|
||||||
@@ -122,17 +123,19 @@ CountableSessionStore = Union[CountingInMemorySessionStore, CountingRedisSession
|
|||||||
|
|
||||||
|
|
||||||
class TelemetryMixin(KayaMixin):
|
class TelemetryMixin(KayaMixin):
|
||||||
"""Application gauges and log export on top of kaya-otel.
|
"""Application gauges, DB instrumentation and log export.
|
||||||
|
|
||||||
The meter and logger providers are built by
|
The meter, tracer and logger providers are built by
|
||||||
:func:`telemetry_from_settings` and injected: kaya-otel only shuts
|
:func:`telemetry_from_settings` and injected: kaya-otel only shuts
|
||||||
down providers it created itself, so this mixin owns their shutdown.
|
down providers it created itself, so this mixin owns their shutdown
|
||||||
|
(and uninstruments the database clients it instrumented).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
meter_provider: "MeterProvider",
|
meter_provider: "MeterProvider",
|
||||||
|
tracer_provider: "TracerProvider",
|
||||||
logger_provider: Optional["LoggerProvider"],
|
logger_provider: Optional["LoggerProvider"],
|
||||||
log_handler: Optional[logging.Handler],
|
log_handler: Optional[logging.Handler],
|
||||||
session_store: CountableSessionStore,
|
session_store: CountableSessionStore,
|
||||||
@@ -140,6 +143,7 @@ class TelemetryMixin(KayaMixin):
|
|||||||
poll_interval_seconds: float,
|
poll_interval_seconds: float,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._meter_provider = meter_provider
|
self._meter_provider = meter_provider
|
||||||
|
self._tracer_provider = tracer_provider
|
||||||
self._logger_provider = logger_provider
|
self._logger_provider = logger_provider
|
||||||
self._log_handler = log_handler
|
self._log_handler = log_handler
|
||||||
self._session_store = session_store
|
self._session_store = session_store
|
||||||
@@ -149,8 +153,11 @@ class TelemetryMixin(KayaMixin):
|
|||||||
self._ongoing_games: Optional[int] = None
|
self._ongoing_games: Optional[int] = None
|
||||||
self._poll_task: Optional[asyncio.Task] = None
|
self._poll_task: Optional[asyncio.Task] = None
|
||||||
self._instrumentor: Optional[Any] = None
|
self._instrumentor: Optional[Any] = None
|
||||||
|
self._db_instrumentors: list = []
|
||||||
|
|
||||||
def apply(self, app: KayaApp) -> None:
|
def apply(self, app: KayaApp) -> None:
|
||||||
|
from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor
|
||||||
|
from opentelemetry.instrumentation.redis import RedisInstrumentor
|
||||||
from opentelemetry.instrumentation.system_metrics import (
|
from opentelemetry.instrumentation.system_metrics import (
|
||||||
SystemMetricsInstrumentor,
|
SystemMetricsInstrumentor,
|
||||||
)
|
)
|
||||||
@@ -160,6 +167,16 @@ class TelemetryMixin(KayaMixin):
|
|||||||
self._instrumentor = instrumentor
|
self._instrumentor = instrumentor
|
||||||
instrumentor.instrument(meter_provider=self._meter_provider)
|
instrumentor.instrument(meter_provider=self._meter_provider)
|
||||||
|
|
||||||
|
# Query spans for the Postgres (asyncpg, used by Tortoise) and Redis
|
||||||
|
# clients. Redis command values are redacted by the instrumentation
|
||||||
|
# (``SET ? ?``); asyncpg spans carry the parameterized SQL only.
|
||||||
|
self._db_instrumentors = [
|
||||||
|
AsyncPGInstrumentor(capture_parameters=False),
|
||||||
|
RedisInstrumentor(),
|
||||||
|
]
|
||||||
|
for db_instrumentor in self._db_instrumentors:
|
||||||
|
db_instrumentor.instrument(tracer_provider=self._tracer_provider)
|
||||||
|
|
||||||
meter = self._meter_provider.get_meter(__name__)
|
meter = self._meter_provider.get_meter(__name__)
|
||||||
|
|
||||||
def active_users(options: CallbackOptions):
|
def active_users(options: CallbackOptions):
|
||||||
@@ -192,12 +209,16 @@ class TelemetryMixin(KayaMixin):
|
|||||||
if self._poll_task is not None:
|
if self._poll_task is not None:
|
||||||
self._poll_task.cancel()
|
self._poll_task.cancel()
|
||||||
self._poll_task = None
|
self._poll_task = None
|
||||||
|
for db_instrumentor in self._db_instrumentors:
|
||||||
|
db_instrumentor.uninstrument()
|
||||||
|
self._db_instrumentors = []
|
||||||
if self._log_handler is not None:
|
if self._log_handler is not None:
|
||||||
logging.getLogger().removeHandler(self._log_handler)
|
logging.getLogger().removeHandler(self._log_handler)
|
||||||
self._log_handler.close()
|
self._log_handler.close()
|
||||||
self._log_handler = None
|
self._log_handler = None
|
||||||
if self._logger_provider is not None:
|
if self._logger_provider is not None:
|
||||||
self._logger_provider.shutdown()
|
self._logger_provider.shutdown()
|
||||||
|
self._tracer_provider.shutdown()
|
||||||
self._meter_provider.shutdown()
|
self._meter_provider.shutdown()
|
||||||
|
|
||||||
async def _poll(self) -> None:
|
async def _poll(self) -> None:
|
||||||
@@ -222,6 +243,7 @@ class Telemetry:
|
|||||||
"""The providers and mixin built from the ``OTEL_*`` settings."""
|
"""The providers and mixin built from the ``OTEL_*`` settings."""
|
||||||
|
|
||||||
meter_provider: "MeterProvider"
|
meter_provider: "MeterProvider"
|
||||||
|
tracer_provider: "TracerProvider"
|
||||||
logger_provider: Optional["LoggerProvider"]
|
logger_provider: Optional["LoggerProvider"]
|
||||||
log_handler: Optional[logging.Handler]
|
log_handler: Optional[logging.Handler]
|
||||||
mixin: TelemetryMixin
|
mixin: TelemetryMixin
|
||||||
@@ -232,7 +254,7 @@ def telemetry_from_settings(
|
|||||||
session_store: CountableSessionStore,
|
session_store: CountableSessionStore,
|
||||||
game_store: GameStore,
|
game_store: GameStore,
|
||||||
) -> Optional[Telemetry]:
|
) -> Optional[Telemetry]:
|
||||||
"""Build the metric/log providers, or ``None`` when telemetry is off.
|
"""Build the metric/trace/log providers, or ``None`` when telemetry is off.
|
||||||
|
|
||||||
Only called when ``OTEL_ENABLED`` is set; the OpenTelemetry imports
|
Only called when ``OTEL_ENABLED`` is set; the OpenTelemetry imports
|
||||||
are lazy so the default install does not need the ``otel`` extra.
|
are lazy so the default install does not need the ``otel`` extra.
|
||||||
@@ -246,11 +268,16 @@ def telemetry_from_settings(
|
|||||||
from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
|
from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
|
||||||
OTLPMetricExporter,
|
OTLPMetricExporter,
|
||||||
)
|
)
|
||||||
|
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
||||||
|
OTLPSpanExporter,
|
||||||
|
)
|
||||||
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
|
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
|
||||||
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
||||||
from opentelemetry.sdk.metrics import MeterProvider
|
from opentelemetry.sdk.metrics import MeterProvider
|
||||||
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
||||||
from opentelemetry.sdk.resources import Resource
|
from opentelemetry.sdk.resources import Resource
|
||||||
|
from opentelemetry.sdk.trace import TracerProvider
|
||||||
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"OTEL_ENABLED is set but the OpenTelemetry dependencies are not "
|
"OTEL_ENABLED is set but the OpenTelemetry dependencies are not "
|
||||||
@@ -260,6 +287,15 @@ def telemetry_from_settings(
|
|||||||
endpoint = settings.otel_exporter_endpoint
|
endpoint = settings.otel_exporter_endpoint
|
||||||
headers = otel_headers(settings)
|
headers = otel_headers(settings)
|
||||||
resource = Resource.create({"service.name": settings.otel_service_name})
|
resource = Resource.create({"service.name": settings.otel_service_name})
|
||||||
|
tracer_provider = TracerProvider(resource=resource)
|
||||||
|
tracer_provider.add_span_processor(
|
||||||
|
BatchSpanProcessor(
|
||||||
|
OTLPSpanExporter(
|
||||||
|
endpoint=_signal_endpoint(endpoint, "v1/traces"),
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
meter_provider = MeterProvider(
|
meter_provider = MeterProvider(
|
||||||
resource=resource,
|
resource=resource,
|
||||||
metric_readers=[
|
metric_readers=[
|
||||||
@@ -286,6 +322,7 @@ def telemetry_from_settings(
|
|||||||
logging.getLogger().addHandler(handler)
|
logging.getLogger().addHandler(handler)
|
||||||
mixin = TelemetryMixin(
|
mixin = TelemetryMixin(
|
||||||
meter_provider=meter_provider,
|
meter_provider=meter_provider,
|
||||||
|
tracer_provider=tracer_provider,
|
||||||
logger_provider=logger_provider,
|
logger_provider=logger_provider,
|
||||||
log_handler=handler,
|
log_handler=handler,
|
||||||
session_store=session_store,
|
session_store=session_store,
|
||||||
@@ -294,6 +331,7 @@ def telemetry_from_settings(
|
|||||||
)
|
)
|
||||||
return Telemetry(
|
return Telemetry(
|
||||||
meter_provider=meter_provider,
|
meter_provider=meter_provider,
|
||||||
|
tracer_provider=tracer_provider,
|
||||||
logger_provider=logger_provider,
|
logger_provider=logger_provider,
|
||||||
log_handler=handler,
|
log_handler=handler,
|
||||||
mixin=mixin,
|
mixin=mixin,
|
||||||
|
|||||||
+15
-2
@@ -94,6 +94,8 @@ opentelemetry-api==1.45.0
|
|||||||
# opentelemetry-exporter-http-transport
|
# opentelemetry-exporter-http-transport
|
||||||
# opentelemetry-exporter-otlp-proto-http
|
# opentelemetry-exporter-otlp-proto-http
|
||||||
# opentelemetry-instrumentation
|
# opentelemetry-instrumentation
|
||||||
|
# opentelemetry-instrumentation-asyncpg
|
||||||
|
# opentelemetry-instrumentation-redis
|
||||||
# opentelemetry-instrumentation-system-metrics
|
# opentelemetry-instrumentation-system-metrics
|
||||||
# opentelemetry-sdk
|
# opentelemetry-sdk
|
||||||
# opentelemetry-semantic-conventions
|
# opentelemetry-semantic-conventions
|
||||||
@@ -106,7 +108,14 @@ opentelemetry-exporter-otlp-proto-common==1.45.0
|
|||||||
opentelemetry-exporter-otlp-proto-http==1.45.0
|
opentelemetry-exporter-otlp-proto-http==1.45.0
|
||||||
# via kaya-otel
|
# via kaya-otel
|
||||||
opentelemetry-instrumentation==0.66b0
|
opentelemetry-instrumentation==0.66b0
|
||||||
# via opentelemetry-instrumentation-system-metrics
|
# via
|
||||||
|
# opentelemetry-instrumentation-asyncpg
|
||||||
|
# opentelemetry-instrumentation-redis
|
||||||
|
# opentelemetry-instrumentation-system-metrics
|
||||||
|
opentelemetry-instrumentation-asyncpg==0.66b0
|
||||||
|
# via tavolo-app (pyproject.toml)
|
||||||
|
opentelemetry-instrumentation-redis==0.66b0
|
||||||
|
# via tavolo-app (pyproject.toml)
|
||||||
opentelemetry-instrumentation-system-metrics==0.66b0
|
opentelemetry-instrumentation-system-metrics==0.66b0
|
||||||
# via tavolo-app (pyproject.toml)
|
# via tavolo-app (pyproject.toml)
|
||||||
opentelemetry-proto==1.45.0
|
opentelemetry-proto==1.45.0
|
||||||
@@ -121,6 +130,8 @@ opentelemetry-sdk==1.45.0
|
|||||||
opentelemetry-semantic-conventions==0.66b0
|
opentelemetry-semantic-conventions==0.66b0
|
||||||
# via
|
# via
|
||||||
# opentelemetry-instrumentation
|
# opentelemetry-instrumentation
|
||||||
|
# opentelemetry-instrumentation-asyncpg
|
||||||
|
# opentelemetry-instrumentation-redis
|
||||||
# opentelemetry-instrumentation-system-metrics
|
# opentelemetry-instrumentation-system-metrics
|
||||||
# opentelemetry-sdk
|
# opentelemetry-sdk
|
||||||
packaging==26.3
|
packaging==26.3
|
||||||
@@ -165,4 +176,6 @@ typing-extensions==4.16.0
|
|||||||
urllib3==2.8.0
|
urllib3==2.8.0
|
||||||
# via opentelemetry-exporter-http-transport
|
# via opentelemetry-exporter-http-transport
|
||||||
wrapt==2.5.0
|
wrapt==2.5.0
|
||||||
# via opentelemetry-instrumentation
|
# via
|
||||||
|
# opentelemetry-instrumentation
|
||||||
|
# opentelemetry-instrumentation-redis
|
||||||
|
|||||||
@@ -78,6 +78,15 @@ class OTelMixinFromSettingsTests(unittest.TestCase):
|
|||||||
assert isinstance(mixin, _StubOTelMixin)
|
assert isinstance(mixin, _StubOTelMixin)
|
||||||
self.assertIs(provider, mixin.kwargs["meter_provider"])
|
self.assertIs(provider, mixin.kwargs["meter_provider"])
|
||||||
|
|
||||||
|
def test_tracer_provider_is_passed_through(self):
|
||||||
|
with _stub_kaya_otel():
|
||||||
|
provider = object()
|
||||||
|
mixin = otel_mixin_from_settings(
|
||||||
|
_settings({"OTEL_ENABLED": "1"}), tracer_provider=provider
|
||||||
|
)
|
||||||
|
assert isinstance(mixin, _StubOTelMixin)
|
||||||
|
self.assertIs(provider, mixin.kwargs["tracer_provider"])
|
||||||
|
|
||||||
def test_excluded_paths_are_passed_through(self):
|
def test_excluded_paths_are_passed_through(self):
|
||||||
with _stub_kaya_otel():
|
with _stub_kaya_otel():
|
||||||
mixin = otel_mixin_from_settings(_settings({
|
mixin = otel_mixin_from_settings(_settings({
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
from types import SimpleNamespace
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
@@ -33,6 +34,11 @@ try:
|
|||||||
from opentelemetry.sdk.metrics import MeterProvider
|
from opentelemetry.sdk.metrics import MeterProvider
|
||||||
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
|
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
|
||||||
from opentelemetry.sdk.resources import Resource
|
from opentelemetry.sdk.resources import Resource
|
||||||
|
from opentelemetry.sdk.trace import TracerProvider
|
||||||
|
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
||||||
|
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
|
||||||
|
InMemorySpanExporter,
|
||||||
|
)
|
||||||
|
|
||||||
HAVE_OTEL = True
|
HAVE_OTEL = True
|
||||||
except ImportError: # pragma: no cover - installs without the otel extra
|
except ImportError: # pragma: no cover - installs without the otel extra
|
||||||
@@ -148,19 +154,25 @@ def _metric_points(reader) -> dict:
|
|||||||
|
|
||||||
@unittest.skipUnless(HAVE_OTEL, "tavolo-app[otel] not installed")
|
@unittest.skipUnless(HAVE_OTEL, "tavolo-app[otel] not installed")
|
||||||
class MetricExportTest(unittest.TestCase):
|
class MetricExportTest(unittest.TestCase):
|
||||||
# SystemMetricsInstrumentor is a process-wide singleton whose callbacks
|
# The instrumentation packages are process-wide singletons whose
|
||||||
# cannot be re-registered, so the provider and mixin are built once for
|
# callbacks cannot be re-registered, so the providers and mixin are
|
||||||
# the whole class (as the application does).
|
# built once for the whole class (as the application does).
|
||||||
sessions = _FakeSessions(0)
|
sessions = _FakeSessions(0)
|
||||||
games = _FakeGames(0)
|
games = _FakeGames(0)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls) -> None:
|
def setUpClass(cls) -> None:
|
||||||
cls.reader = InMemoryMetricReader()
|
|
||||||
resource = Resource.create({"service.name": "test"})
|
resource = Resource.create({"service.name": "test"})
|
||||||
|
cls.reader = InMemoryMetricReader()
|
||||||
cls.provider = MeterProvider(resource=resource, metric_readers=[cls.reader])
|
cls.provider = MeterProvider(resource=resource, metric_readers=[cls.reader])
|
||||||
|
cls.span_exporter = InMemorySpanExporter()
|
||||||
|
cls.tracer_provider = TracerProvider(resource=resource)
|
||||||
|
cls.tracer_provider.add_span_processor(
|
||||||
|
SimpleSpanProcessor(cls.span_exporter)
|
||||||
|
)
|
||||||
cls.mixin = TelemetryMixin(
|
cls.mixin = TelemetryMixin(
|
||||||
meter_provider=cls.provider,
|
meter_provider=cls.provider,
|
||||||
|
tracer_provider=cls.tracer_provider,
|
||||||
logger_provider=None,
|
logger_provider=None,
|
||||||
log_handler=None,
|
log_handler=None,
|
||||||
session_store=cls.sessions,
|
session_store=cls.sessions,
|
||||||
@@ -171,7 +183,7 @@ class MetricExportTest(unittest.TestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls) -> None:
|
def tearDownClass(cls) -> None:
|
||||||
cls.provider.shutdown()
|
cls.mixin.shutdown(None) # type: ignore[arg-type]
|
||||||
|
|
||||||
@async_test
|
@async_test
|
||||||
async def test_app_gauges_report_polled_counts(self) -> None:
|
async def test_app_gauges_report_polled_counts(self) -> None:
|
||||||
@@ -218,6 +230,36 @@ class MetricExportTest(unittest.TestCase):
|
|||||||
# Host-level metrics are deliberately not configured.
|
# Host-level metrics are deliberately not configured.
|
||||||
self.assertNotIn("system.cpu.time", points)
|
self.assertNotIn("system.cpu.time", points)
|
||||||
|
|
||||||
|
@async_test
|
||||||
|
async def test_database_clients_are_instrumented(self) -> None:
|
||||||
|
import asyncpg.connection
|
||||||
|
import redis.asyncio
|
||||||
|
|
||||||
|
self.assertTrue(hasattr(redis.asyncio.Redis.execute_command, "__wrapped__"))
|
||||||
|
self.assertTrue(hasattr(asyncpg.connection.Connection.fetch, "__wrapped__"))
|
||||||
|
|
||||||
|
# Exercise each wrapper through stubs: the downstream call fails (no
|
||||||
|
# server), but the span is recorded before it does.
|
||||||
|
client = object.__new__(redis.asyncio.Redis)
|
||||||
|
client.connection_pool = SimpleNamespace(
|
||||||
|
connection_kwargs={"host": "127.0.0.1", "port": 6379, "db": 0}
|
||||||
|
)
|
||||||
|
with self.assertRaises(AttributeError):
|
||||||
|
await client.execute_command("GET", "key")
|
||||||
|
|
||||||
|
connection = object.__new__(asyncpg.connection.Connection)
|
||||||
|
connection._aborted = True # keep __del__ quiet on the stub
|
||||||
|
with self.assertRaises(Exception):
|
||||||
|
await connection.fetch("SELECT 1")
|
||||||
|
|
||||||
|
spans = {
|
||||||
|
span.name: span for span in self.span_exporter.get_finished_spans()
|
||||||
|
}
|
||||||
|
self.assertEqual("GET ?", spans["GET"].attributes["db.statement"])
|
||||||
|
self.assertEqual("redis", spans["GET"].attributes["db.system"])
|
||||||
|
self.assertEqual("SELECT 1", spans["SELECT"].attributes["db.statement"])
|
||||||
|
self.assertEqual("postgresql", spans["SELECT"].attributes["db.system"])
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipUnless(HAVE_OTEL, "tavolo-app[otel] not installed")
|
@unittest.skipUnless(HAVE_OTEL, "tavolo-app[otel] not installed")
|
||||||
class LogExportTest(unittest.TestCase):
|
class LogExportTest(unittest.TestCase):
|
||||||
|
|||||||
Reference in New Issue
Block a user