Compare commits
16
Commits
main
..
a606f14550
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a606f14550 | ||
|
|
3da0c463de | ||
|
|
26464bce43 | ||
|
|
8a2941eccc
|
||
|
|
57c6ffb032
|
||
|
|
c10d45523a
|
||
|
|
bbd12ce674 | ||
|
|
c40ebd84d8 | ||
|
|
658e3d6ec7
|
||
|
|
5e4e1310b4
|
||
|
|
db7ba30d13
|
||
|
|
f7fa8e78e5
|
||
|
|
5b9e998739 | ||
|
|
5dc3f4cb17 | ||
|
|
22ef689092 | ||
|
|
aa1fecae43 |
@@ -71,13 +71,6 @@ 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): traces + metrics via OTLP/HTTP, disabled
|
|
||||||
# unless OTEL_ENABLED is set. Requires the otel extra in the image.
|
|
||||||
# OTEL_ENABLED: "true"
|
|
||||||
# OTEL_SERVICE_NAME: "tavolo"
|
|
||||||
# OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.observability:4318"
|
|
||||||
# OTEL_EXPORTER_OTLP_HEADERS: "Authorization=Bearer ..."
|
|
||||||
# OTEL_EXCLUDED_PATHS: "/api/health" # default; paths skipped by tracing
|
|
||||||
# OIDC (provider lives in another namespace).
|
# OIDC (provider lives in another namespace).
|
||||||
OIDC_CLIENT_ID: tavolo
|
OIDC_CLIENT_ID: tavolo
|
||||||
OIDC_POST_LOGIN_REDIRECT: /
|
OIDC_POST_LOGIN_REDIRECT: /
|
||||||
|
|||||||
@@ -122,13 +122,6 @@ 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): disabled unless OTEL_ENABLED is set.
|
|
||||||
# Requires the otel extra in the image (see server/pyproject.toml).
|
|
||||||
OTEL_ENABLED: ${OTEL_ENABLED:-}
|
|
||||||
OTEL_SERVICE_NAME: ${OTEL_SERVICE_NAME:-}
|
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-}
|
|
||||||
OTEL_EXPORTER_OTLP_HEADERS: ${OTEL_EXPORTER_OTLP_HEADERS:-}
|
|
||||||
OTEL_EXCLUDED_PATHS: ${OTEL_EXCLUDED_PATHS:-}
|
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${APP_PORT:-8080}:8080"
|
- "127.0.0.1:${APP_PORT:-8080}:8080"
|
||||||
|
|
||||||
|
|||||||
+6
-10
@@ -48,21 +48,17 @@ RUN --mount=type=cache,target=/var/cache/apk \
|
|||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
|
COPY server/pyproject.toml server/README.md server/requirements.txt ./
|
||||||
COPY server/requirements.txt ./
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
|
||||||
python3 -m venv /opt/venv \
|
|
||||||
&& /opt/venv/bin/pip install --upgrade pip \
|
|
||||||
&& /opt/venv/bin/pip install -r requirements.txt
|
|
||||||
|
|
||||||
COPY server/pyproject.toml server/README.md ./
|
|
||||||
COPY server/src/ ./src/
|
COPY server/src/ ./src/
|
||||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
|
||||||
/opt/venv/bin/pip install .
|
|
||||||
# aerich migration files are a release artifact: the db-migrate compose
|
# aerich migration files are a release artifact: the db-migrate compose
|
||||||
# service runs `aerich upgrade` from this image before the app starts.
|
# service runs `aerich upgrade` from this image before the app starts.
|
||||||
COPY server/migrations/ ./migrations/
|
COPY server/migrations/ ./migrations/
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||||
|
python3 -m venv /opt/venv \
|
||||||
|
&& /opt/venv/bin/pip install --upgrade pip \
|
||||||
|
&& /opt/venv/bin/pip install -r requirements.txt .
|
||||||
|
|
||||||
# --- Runtime ---------------------------------------------------------------
|
# --- Runtime ---------------------------------------------------------------
|
||||||
FROM alpine:3.24
|
FROM alpine:3.24
|
||||||
|
|
||||||
|
|||||||
@@ -69,11 +69,6 @@ All configuration comes from environment variables (see `.env.example`):
|
|||||||
| `CORS_ALLOW_CREDENTIALS` | `false` | `1`/`true`/`yes`/`on` allow cookies/credentials on cross-origin requests |
|
| `CORS_ALLOW_CREDENTIALS` | `false` | `1`/`true`/`yes`/`on` allow cookies/credentials on cross-origin requests |
|
||||||
| `CORS_EXPOSE_HEADERS` | unset | Comma-separated response headers exposed to the browser |
|
| `CORS_EXPOSE_HEADERS` | unset | Comma-separated response headers exposed to the browser |
|
||||||
| `CORS_MAX_AGE` | `600` | Seconds browsers may cache the preflight response |
|
| `CORS_MAX_AGE` | `600` | Seconds browsers may cache the preflight response |
|
||||||
| `OTEL_ENABLED` | `false` | `1`/`true`/`yes`/`on` enable OpenTelemetry traces and metrics (requires the `otel` extra, i.e. `pip install tavolo[otel]`) |
|
|
||||||
| `OTEL_SERVICE_NAME` | `tavolo` | `service.name` resource attribute of the exported telemetry |
|
|
||||||
| `OTEL_EXPORTER_OTLP_ENDPOINT` | unset | Base URL of an OTLP/HTTP collector (e.g. `http://localhost:4318`); unset uses the exporter default |
|
|
||||||
| `OTEL_EXPORTER_OTLP_HEADERS` | unset | Comma-separated `key=value` headers sent to the collector (e.g. authentication) |
|
|
||||||
| `OTEL_EXCLUDED_PATHS` | `/api/health` | Comma-separated paths excluded from tracing and metrics (exact matches) |
|
|
||||||
| `APP_HOST` / `APP_PORT` | `0.0.0.0` / `8080` | Bind address |
|
| `APP_HOST` / `APP_PORT` | `0.0.0.0` / `8080` | Bind address |
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|||||||
@@ -32,9 +32,6 @@ dev = [
|
|||||||
"mypy",
|
"mypy",
|
||||||
"httpx-ws",
|
"httpx-ws",
|
||||||
]
|
]
|
||||||
otel = [
|
|
||||||
"kaya-otel>=0.0.4",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["src"]
|
where = ["src"]
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ idna==3.19
|
|||||||
# httpx
|
# httpx
|
||||||
iso8601==2.1.0
|
iso8601==2.1.0
|
||||||
# via tortoise-orm
|
# via tortoise-orm
|
||||||
kaya-core==0.0.4
|
kaya-core==0.0.3
|
||||||
# via
|
# via
|
||||||
# kaya-cors
|
# kaya-cors
|
||||||
# kaya-oidc
|
# kaya-oidc
|
||||||
@@ -58,20 +58,20 @@ kaya-core==0.0.4
|
|||||||
# kaya-rsgi
|
# kaya-rsgi
|
||||||
# kaya-session
|
# kaya-session
|
||||||
# tavolo (pyproject.toml)
|
# tavolo (pyproject.toml)
|
||||||
kaya-cors==0.0.4
|
kaya-cors==0.0.3
|
||||||
# via tavolo (pyproject.toml)
|
# via tavolo (pyproject.toml)
|
||||||
kaya-oidc==0.0.4
|
kaya-oidc==0.0.3
|
||||||
# via tavolo (pyproject.toml)
|
# via tavolo (pyproject.toml)
|
||||||
kaya-openapi==0.0.4
|
kaya-openapi==0.0.3
|
||||||
# via tavolo (pyproject.toml)
|
# via tavolo (pyproject.toml)
|
||||||
kaya-rsgi==0.0.4
|
kaya-rsgi==0.0.3
|
||||||
# via tavolo (pyproject.toml)
|
# via tavolo (pyproject.toml)
|
||||||
kaya-session==0.0.4
|
kaya-session==0.0.3
|
||||||
# via
|
# via
|
||||||
# kaya-oidc
|
# kaya-oidc
|
||||||
# kaya-session-redis
|
# kaya-session-redis
|
||||||
# tavolo (pyproject.toml)
|
# tavolo (pyproject.toml)
|
||||||
kaya-session-redis==0.0.4
|
kaya-session-redis==0.0.3
|
||||||
# via tavolo (pyproject.toml)
|
# via tavolo (pyproject.toml)
|
||||||
pwo==0.1.2
|
pwo==0.1.2
|
||||||
# via
|
# via
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ Assembles the :class:`~kaya.core.KayaApp` with four mixins:
|
|||||||
``/api/openapi.json`` and a Swagger UI at ``/api/docs``)
|
``/api/openapi.json`` and a Swagger UI at ``/api/docs``)
|
||||||
|
|
||||||
A :class:`~kaya.cors.CorsMixin` is prepended when CORS is configured via the
|
A :class:`~kaya.cors.CorsMixin` is prepended when CORS is configured via the
|
||||||
``CORS_*`` environment variables (see :mod:`tavolo.config`). A
|
``CORS_*`` environment variables (see :mod:`tavolo.config`).
|
||||||
:class:`~kaya.otel.OTelMixin` (optional ``otel`` extra) is prepended when
|
|
||||||
``OTEL_ENABLED`` is set, adding OpenTelemetry traces and metrics.
|
|
||||||
|
|
||||||
Live games are kept in :data:`game_store` (Redis when configured, in-memory
|
Live games are kept in :data:`game_store` (Redis when configured, in-memory
|
||||||
otherwise). Routes and the websocket handlers are registered by importing
|
otherwise). Routes and the websocket handlers are registered by importing
|
||||||
@@ -64,36 +62,6 @@ def cors_mixin_from_settings(settings: Settings) -> Optional[CorsMixin]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def otel_mixin_from_settings(settings: Settings) -> Optional[KayaMixin]:
|
|
||||||
"""Build a :class:`~kaya.otel.OTelMixin` from the OTEL_* settings.
|
|
||||||
|
|
||||||
Returns ``None`` — telemetry disabled — unless ``OTEL_ENABLED`` is
|
|
||||||
truthy. kaya-otel is an optional dependency (the ``otel`` extra), so it
|
|
||||||
is imported lazily here: default installs and the test suite never need
|
|
||||||
the OpenTelemetry packages.
|
|
||||||
"""
|
|
||||||
if not settings.otel_enabled:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
from kaya.otel import OTelMixin
|
|
||||||
except ImportError as exc:
|
|
||||||
raise RuntimeError(
|
|
||||||
"OTEL_ENABLED is set but kaya-otel is not installed; "
|
|
||||||
"install tavolo with the 'otel' extra"
|
|
||||||
) from exc
|
|
||||||
headers = dict(
|
|
||||||
pair.split("=", 1)
|
|
||||||
for pair in (settings.otel_exporter_headers or ())
|
|
||||||
if "=" in pair
|
|
||||||
)
|
|
||||||
return OTelMixin(
|
|
||||||
service_name=settings.otel_service_name,
|
|
||||||
endpoint=settings.otel_exporter_endpoint,
|
|
||||||
headers=headers or None,
|
|
||||||
excluded_paths=settings.otel_excluded_paths,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
session_store: SessionStore
|
session_store: SessionStore
|
||||||
if settings.redis_url is not None:
|
if settings.redis_url is not None:
|
||||||
# Lazy client: no connection is opened until a session is actually
|
# Lazy client: no connection is opened until a session is actually
|
||||||
@@ -137,19 +105,6 @@ tortoise_mixin = TortoiseMixin(
|
|||||||
|
|
||||||
mixins: list[KayaMixin] = [session_mixin, oidc_mixin, tortoise_mixin, openapi_mixin,
|
mixins: list[KayaMixin] = [session_mixin, oidc_mixin, tortoise_mixin, openapi_mixin,
|
||||||
DeadlineSchedulerMixin(game_store)]
|
DeadlineSchedulerMixin(game_store)]
|
||||||
otel_mixin = otel_mixin_from_settings(settings)
|
|
||||||
if otel_mixin is not None:
|
|
||||||
# First in the list: before hooks run in registration order (after hooks
|
|
||||||
# in reverse), so the span covers session loading, OIDC handling and the
|
|
||||||
# handler itself. CORS, when enabled, is still prepended before it so
|
|
||||||
# preflight short-circuits stay untraced.
|
|
||||||
mixins.insert(0, otel_mixin)
|
|
||||||
log.info(
|
|
||||||
"OpenTelemetry enabled: service=%s endpoint=%s",
|
|
||||||
settings.otel_service_name,
|
|
||||||
settings.otel_exporter_endpoint or "(OTLP default)",
|
|
||||||
)
|
|
||||||
|
|
||||||
cors_mixin = cors_mixin_from_settings(settings)
|
cors_mixin = cors_mixin_from_settings(settings)
|
||||||
if cors_mixin is not None:
|
if cors_mixin is not None:
|
||||||
# First in the list: preflight requests are answered before the session
|
# First in the list: preflight requests are answered before the session
|
||||||
|
|||||||
@@ -113,16 +113,6 @@ class Settings:
|
|||||||
cors_allow_credentials: bool
|
cors_allow_credentials: bool
|
||||||
cors_expose_headers: Optional[Tuple[str, ...]]
|
cors_expose_headers: Optional[Tuple[str, ...]]
|
||||||
cors_max_age: int
|
cors_max_age: int
|
||||||
# OpenTelemetry (kaya-otel's OTelMixin). Disabled unless OTEL_ENABLED is
|
|
||||||
# truthy; the exporter endpoint falls back to the OTLP/HTTP default
|
|
||||||
# (localhost:4318) when OTEL_EXPORTER_OTLP_ENDPOINT is unset.
|
|
||||||
otel_enabled: bool
|
|
||||||
otel_service_name: str
|
|
||||||
otel_exporter_endpoint: Optional[str]
|
|
||||||
otel_exporter_headers: Optional[Tuple[str, ...]]
|
|
||||||
# Paths excluded from tracing and metrics (exact matches). Defaults to
|
|
||||||
# the health endpoint, which k8s probes would otherwise spam.
|
|
||||||
otel_excluded_paths: Tuple[str, ...]
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_env() -> "Settings":
|
def from_env() -> "Settings":
|
||||||
@@ -169,14 +159,6 @@ class Settings:
|
|||||||
cors_allow_credentials=_env_bool("CORS_ALLOW_CREDENTIALS"),
|
cors_allow_credentials=_env_bool("CORS_ALLOW_CREDENTIALS"),
|
||||||
cors_expose_headers=_env_list("CORS_EXPOSE_HEADERS"),
|
cors_expose_headers=_env_list("CORS_EXPOSE_HEADERS"),
|
||||||
cors_max_age=int(_env("CORS_MAX_AGE", "600")),
|
cors_max_age=int(_env("CORS_MAX_AGE", "600")),
|
||||||
# OpenTelemetry is opt-in: set OTEL_ENABLED=1 to export traces
|
|
||||||
# and metrics via OTLP/HTTP (requires the ``otel`` extra).
|
|
||||||
otel_enabled=_env_bool("OTEL_ENABLED"),
|
|
||||||
otel_service_name=_env("OTEL_SERVICE_NAME", "tavolo"),
|
|
||||||
otel_exporter_endpoint=os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") or None,
|
|
||||||
# Comma-separated key=value pairs, e.g. "Authorization=Bearer x".
|
|
||||||
otel_exporter_headers=_env_list("OTEL_EXPORTER_OTLP_HEADERS"),
|
|
||||||
otel_excluded_paths=_env_list("OTEL_EXCLUDED_PATHS") or ("/api/health",),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -130,40 +130,5 @@ class CorsSettingsTests(unittest.TestCase):
|
|||||||
self.assertEqual(3600, settings.cors_max_age)
|
self.assertEqual(3600, settings.cors_max_age)
|
||||||
|
|
||||||
|
|
||||||
class OTelSettingsTests(unittest.TestCase):
|
|
||||||
def test_otel_disabled_by_default(self):
|
|
||||||
settings = _settings({})
|
|
||||||
self.assertFalse(settings.otel_enabled)
|
|
||||||
self.assertEqual("tavolo", settings.otel_service_name)
|
|
||||||
self.assertIsNone(settings.otel_exporter_endpoint)
|
|
||||||
self.assertIsNone(settings.otel_exporter_headers)
|
|
||||||
|
|
||||||
def test_otel_enabled_parses_boolean(self):
|
|
||||||
for value in ("1", "true", "TRUE", "yes", "on"):
|
|
||||||
self.assertTrue(_settings({"OTEL_ENABLED": value}).otel_enabled)
|
|
||||||
for value in ("0", "false", "no", "off", "anything-else"):
|
|
||||||
self.assertFalse(_settings({"OTEL_ENABLED": value}).otel_enabled)
|
|
||||||
|
|
||||||
def test_otel_settings_are_passed_through(self):
|
|
||||||
settings = _settings({
|
|
||||||
"OTEL_SERVICE_NAME": "cards",
|
|
||||||
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://collector:4318",
|
|
||||||
"OTEL_EXPORTER_OTLP_HEADERS": "Authorization=Bearer t, X-Tenant=one",
|
|
||||||
})
|
|
||||||
self.assertEqual("cards", settings.otel_service_name)
|
|
||||||
self.assertEqual("http://collector:4318", settings.otel_exporter_endpoint)
|
|
||||||
self.assertEqual(
|
|
||||||
("Authorization=Bearer t", "X-Tenant=one"),
|
|
||||||
settings.otel_exporter_headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_otel_excluded_paths_defaults_to_health_endpoint(self):
|
|
||||||
self.assertEqual(("/api/health",), _settings({}).otel_excluded_paths)
|
|
||||||
|
|
||||||
def test_otel_excluded_paths_parses_comma_separated_list(self):
|
|
||||||
settings = _settings({"OTEL_EXCLUDED_PATHS": "/api/health, /metrics"})
|
|
||||||
self.assertEqual(("/api/health", "/metrics"), settings.otel_excluded_paths)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
"""Unit tests for the OpenTelemetry wiring in :mod:`tavolo.app`.
|
|
||||||
|
|
||||||
The mixin under test is kaya-otel's :class:`~kaya.otel.OTelMixin`, an
|
|
||||||
optional dependency (the ``otel`` extra); these tests only verify that
|
|
||||||
:func:`tavolo.app.otel_mixin_from_settings` maps the ``OTEL_*`` settings
|
|
||||||
onto mixin construction. The ``kaya.otel`` module is stubbed in
|
|
||||||
``sys.modules`` so the suite does not need the extra installed.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import types
|
|
||||||
import unittest
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from tavolo.app import otel_mixin_from_settings
|
|
||||||
from tavolo.config import Settings
|
|
||||||
|
|
||||||
|
|
||||||
def _settings(env: dict) -> Settings:
|
|
||||||
with patch.dict(os.environ, env, clear=True):
|
|
||||||
return Settings.from_env()
|
|
||||||
|
|
||||||
|
|
||||||
class _StubOTelMixin:
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
self.kwargs = kwargs
|
|
||||||
|
|
||||||
|
|
||||||
def _stub_kaya_otel():
|
|
||||||
"""Install a fake ``kaya.otel`` module and return it."""
|
|
||||||
module = types.ModuleType("kaya.otel")
|
|
||||||
module.OTelMixin = _StubOTelMixin # type: ignore[attr-defined]
|
|
||||||
return patch.dict(sys.modules, {"kaya.otel": module})
|
|
||||||
|
|
||||||
|
|
||||||
class OTelMixinFromSettingsTests(unittest.TestCase):
|
|
||||||
def test_disabled_by_default(self):
|
|
||||||
self.assertIsNone(otel_mixin_from_settings(_settings({})))
|
|
||||||
|
|
||||||
def test_enabled_by_otel_enabled(self):
|
|
||||||
with _stub_kaya_otel():
|
|
||||||
mixin = otel_mixin_from_settings(_settings({"OTEL_ENABLED": "1"}))
|
|
||||||
self.assertIsNotNone(mixin)
|
|
||||||
|
|
||||||
def test_settings_are_passed_through(self):
|
|
||||||
with _stub_kaya_otel():
|
|
||||||
mixin = otel_mixin_from_settings(_settings({
|
|
||||||
"OTEL_ENABLED": "true",
|
|
||||||
"OTEL_SERVICE_NAME": "cards",
|
|
||||||
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://collector:4318",
|
|
||||||
"OTEL_EXPORTER_OTLP_HEADERS": "Authorization=Bearer t, X-Tenant=one",
|
|
||||||
}))
|
|
||||||
assert isinstance(mixin, _StubOTelMixin)
|
|
||||||
self.assertEqual({
|
|
||||||
"service_name": "cards",
|
|
||||||
"endpoint": "http://collector:4318",
|
|
||||||
"headers": {"Authorization": "Bearer t", "X-Tenant": "one"},
|
|
||||||
"excluded_paths": ("/api/health",),
|
|
||||||
}, mixin.kwargs)
|
|
||||||
|
|
||||||
def test_defaults_when_only_enabled(self):
|
|
||||||
with _stub_kaya_otel():
|
|
||||||
mixin = otel_mixin_from_settings(_settings({"OTEL_ENABLED": "on"}))
|
|
||||||
assert isinstance(mixin, _StubOTelMixin)
|
|
||||||
self.assertEqual("tavolo", mixin.kwargs["service_name"])
|
|
||||||
self.assertIsNone(mixin.kwargs["endpoint"])
|
|
||||||
self.assertIsNone(mixin.kwargs["headers"])
|
|
||||||
self.assertEqual(("/api/health",), mixin.kwargs["excluded_paths"])
|
|
||||||
|
|
||||||
def test_excluded_paths_are_passed_through(self):
|
|
||||||
with _stub_kaya_otel():
|
|
||||||
mixin = otel_mixin_from_settings(_settings({
|
|
||||||
"OTEL_ENABLED": "1",
|
|
||||||
"OTEL_EXCLUDED_PATHS": "/api/health,/metrics",
|
|
||||||
}))
|
|
||||||
assert isinstance(mixin, _StubOTelMixin)
|
|
||||||
self.assertEqual(("/api/health", "/metrics"), mixin.kwargs["excluded_paths"])
|
|
||||||
|
|
||||||
def test_missing_extra_raises_runtime_error(self):
|
|
||||||
with patch.dict(sys.modules, {"kaya.otel": None}):
|
|
||||||
with self.assertRaises(RuntimeError):
|
|
||||||
otel_mixin_from_settings(_settings({"OTEL_ENABLED": "1"}))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
Reference in New Issue
Block a user