fastapi-observer 1.6.0__tar.gz → 1.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_observer-1.6.0/src/fastapi_observer.egg-info → fastapi_observer-1.7.0}/PKG-INFO +1 -1
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0/src/fastapi_observer.egg-info}/PKG-INFO +1 -1
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapi_observer.egg-info/SOURCES.txt +1 -14
- fastapi_observer-1.7.0/src/fastapiobserver/_version.py +1 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/db_tracing.py +35 -23
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/fastapi.py +3 -1
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/logging/circuit_breaker.py +16 -1
- fastapi_observer-1.7.0/src/fastapiobserver/metrics/__init__.py +144 -0
- fastapi_observer-1.7.0/src/fastapiobserver/metrics/prometheus.py +325 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/middleware/__init__.py +0 -2
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/middleware/request_logging.py +0 -3
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/otel/__init__.py +0 -19
- fastapi_observer-1.7.0/src/fastapiobserver/otel/exclusions.py +29 -0
- fastapi_observer-1.7.0/src/fastapiobserver/otel/lifecycle.py +57 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/otel/logs.py +45 -107
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/otel/metrics.py +10 -31
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/otel/resource.py +27 -111
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/otel/tracing.py +15 -41
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_db_tracing.py +40 -5
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_excluded_url_precedence.py +8 -8
- fastapi_observer-1.7.0/tests/test_metrics_contracts.py +102 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_metrics_optional_dependency.py +9 -21
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_middleware.py +16 -24
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_negotiate_content_type.py +41 -13
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_otel_integration.py +81 -6
- fastapi_observer-1.7.0/tests/test_otel_lifecycle.py +67 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_otel_log_correlation.py +1 -2
- fastapi_observer-1.7.0/tests/test_otel_logs_pipeline.py +144 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_otlp_export_integration.py +0 -2
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_sink_circuit_breaker.py +28 -0
- fastapi_observer-1.6.0/src/fastapiobserver/_version.py +0 -1
- fastapi_observer-1.6.0/src/fastapiobserver/metrics/__init__.py +0 -41
- fastapi_observer-1.6.0/src/fastapiobserver/metrics/builder.py +0 -48
- fastapi_observer-1.6.0/src/fastapiobserver/metrics/contracts.py +0 -39
- fastapi_observer-1.6.0/src/fastapiobserver/metrics/endpoint.py +0 -180
- fastapi_observer-1.6.0/src/fastapiobserver/metrics/noop.py +0 -15
- fastapi_observer-1.6.0/src/fastapiobserver/metrics/pathing.py +0 -18
- fastapi_observer-1.6.0/src/fastapiobserver/metrics/prometheus/__init__.py +0 -21
- fastapi_observer-1.6.0/src/fastapiobserver/metrics/prometheus/backend.py +0 -126
- fastapi_observer-1.6.0/src/fastapiobserver/metrics/prometheus/client.py +0 -19
- fastapi_observer-1.6.0/src/fastapiobserver/metrics/prometheus/collector.py +0 -170
- fastapi_observer-1.6.0/src/fastapiobserver/metrics/prometheus/exemplars.py +0 -19
- fastapi_observer-1.6.0/src/fastapiobserver/metrics/prometheus/multiprocess.py +0 -68
- fastapi_observer-1.6.0/src/fastapiobserver/metrics/registry.py +0 -42
- fastapi_observer-1.6.0/src/fastapiobserver/middleware/span_errors.py +0 -34
- fastapi_observer-1.6.0/src/fastapiobserver/otel/exclusions.py +0 -45
- fastapi_observer-1.6.0/src/fastapiobserver/otel/lifecycle.py +0 -159
- fastapi_observer-1.6.0/src/fastapiobserver/otel/types.py +0 -49
- fastapi_observer-1.6.0/tests/test_metrics_contracts.py +0 -182
- fastapi_observer-1.6.0/tests/test_otel_lifecycle.py +0 -98
- fastapi_observer-1.6.0/tests/test_otel_logs_pipeline.py +0 -73
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/LICENSE +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/README.md +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/pyproject.toml +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/setup.cfg +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapi_observer.egg-info/dependency_links.txt +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapi_observer.egg-info/requires.txt +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapi_observer.egg-info/top_level.txt +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/__init__.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/audit/__init__.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/audit/formatter.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/audit/providers.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/audit/verify.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/config.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/control_plane.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/integrations/__init__.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/integrations/strawberry.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/logging/__init__.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/logging/filters.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/logging/formatter.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/logging/queueing.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/logging/setup.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/logging/state.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/loguru.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/middleware/body_capture.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/middleware/context.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/middleware/events.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/middleware/headers.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/middleware/ip.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/middleware/metrics.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/otel/settings.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/plugins.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/profiles.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/propagation.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/py.typed +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/request_context.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/security/__init__.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/security/normalize.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/security/policy.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/security/proxies.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/security/redaction.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/sinks/__init__.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/sinks/builtin.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/sinks/discovery.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/sinks/factory.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/sinks/logtail/__init__.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/sinks/logtail/dlq.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/sinks/logtail/handler.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/sinks/logtail/sink.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/sinks/protocol.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/sinks/registry.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/sinks/stats.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/utils.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_audit_chain.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_body_media_allowlist.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_config_settings.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_dx_parity.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_fastapi_install.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_fork_queue_listener.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_graphql_integrations.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_lifespan_teardown.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_log_queue_controls.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_logging_schema_contract.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_logtail_dlq.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_loguru_bridge.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_otel_logs_from_env.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_otel_metrics_from_env.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_plugin_isolation.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_profiles.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_propagation.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_redaction_presets.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_route_template_cardinality.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_runtime_control_auth.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_security_audit_fixes.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_security_redaction.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_sinks.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_trust_boundary.py +0 -0
- {fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/tests/test_utils_refactors.py +0 -0
|
@@ -33,18 +33,7 @@ src/fastapiobserver/logging/queueing.py
|
|
|
33
33
|
src/fastapiobserver/logging/setup.py
|
|
34
34
|
src/fastapiobserver/logging/state.py
|
|
35
35
|
src/fastapiobserver/metrics/__init__.py
|
|
36
|
-
src/fastapiobserver/metrics/
|
|
37
|
-
src/fastapiobserver/metrics/contracts.py
|
|
38
|
-
src/fastapiobserver/metrics/endpoint.py
|
|
39
|
-
src/fastapiobserver/metrics/noop.py
|
|
40
|
-
src/fastapiobserver/metrics/pathing.py
|
|
41
|
-
src/fastapiobserver/metrics/registry.py
|
|
42
|
-
src/fastapiobserver/metrics/prometheus/__init__.py
|
|
43
|
-
src/fastapiobserver/metrics/prometheus/backend.py
|
|
44
|
-
src/fastapiobserver/metrics/prometheus/client.py
|
|
45
|
-
src/fastapiobserver/metrics/prometheus/collector.py
|
|
46
|
-
src/fastapiobserver/metrics/prometheus/exemplars.py
|
|
47
|
-
src/fastapiobserver/metrics/prometheus/multiprocess.py
|
|
36
|
+
src/fastapiobserver/metrics/prometheus.py
|
|
48
37
|
src/fastapiobserver/middleware/__init__.py
|
|
49
38
|
src/fastapiobserver/middleware/body_capture.py
|
|
50
39
|
src/fastapiobserver/middleware/context.py
|
|
@@ -53,7 +42,6 @@ src/fastapiobserver/middleware/headers.py
|
|
|
53
42
|
src/fastapiobserver/middleware/ip.py
|
|
54
43
|
src/fastapiobserver/middleware/metrics.py
|
|
55
44
|
src/fastapiobserver/middleware/request_logging.py
|
|
56
|
-
src/fastapiobserver/middleware/span_errors.py
|
|
57
45
|
src/fastapiobserver/otel/__init__.py
|
|
58
46
|
src/fastapiobserver/otel/exclusions.py
|
|
59
47
|
src/fastapiobserver/otel/lifecycle.py
|
|
@@ -62,7 +50,6 @@ src/fastapiobserver/otel/metrics.py
|
|
|
62
50
|
src/fastapiobserver/otel/resource.py
|
|
63
51
|
src/fastapiobserver/otel/settings.py
|
|
64
52
|
src/fastapiobserver/otel/tracing.py
|
|
65
|
-
src/fastapiobserver/otel/types.py
|
|
66
53
|
src/fastapiobserver/security/__init__.py
|
|
67
54
|
src/fastapiobserver/security/normalize.py
|
|
68
55
|
src/fastapiobserver/security/policy.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.7.0"
|
|
@@ -12,7 +12,7 @@ Usage::
|
|
|
12
12
|
|
|
13
13
|
engine = create_engine("postgresql://...")
|
|
14
14
|
instrument_sqlalchemy(engine)
|
|
15
|
-
# SELECT * FROM users /*traceparent='00-abc123...'
|
|
15
|
+
# SELECT * FROM users /*db_driver='psycopg2',traceparent='00-abc123...'*/
|
|
16
16
|
|
|
17
17
|
Or for async engines::
|
|
18
18
|
|
|
@@ -35,9 +35,12 @@ _DEFAULT_COMMENTER_OPTIONS: dict[str, bool] = {
|
|
|
35
35
|
"opentelemetry_values": True, # injects traceparent
|
|
36
36
|
"db_driver": True,
|
|
37
37
|
"db_framework": False, # noisy — omit by default
|
|
38
|
-
"route": True,
|
|
39
38
|
}
|
|
40
39
|
|
|
40
|
+
# OpenTelemetry's instrumentor is a singleton that ignores a second
|
|
41
|
+
# ``instrument()`` call, so every call re-instruments all engines seen so far.
|
|
42
|
+
_ENGINES: list[Any] = []
|
|
43
|
+
|
|
41
44
|
_PACKAGE_HINT = "fastapi-observer[otel-sqlalchemy]"
|
|
42
45
|
|
|
43
46
|
|
|
@@ -58,38 +61,46 @@ def instrument_sqlalchemy(
|
|
|
58
61
|
context to every query.
|
|
59
62
|
commenter_options:
|
|
60
63
|
Key-value pairs controlling which fields appear in the SQL
|
|
61
|
-
comment
|
|
62
|
-
``
|
|
64
|
+
comment (``opentelemetry_values``, ``db_driver``, ``db_framework``).
|
|
65
|
+
Defaults to the trace context and ``db_driver``.
|
|
66
|
+
|
|
67
|
+
Call it once per engine. The commenter settings of the latest call apply
|
|
68
|
+
to every instrumented engine.
|
|
63
69
|
"""
|
|
64
70
|
try:
|
|
65
71
|
instrumentor_module = lazy_import(
|
|
66
72
|
"opentelemetry.instrumentation.sqlalchemy",
|
|
67
73
|
package_hint=_PACKAGE_HINT,
|
|
68
74
|
)
|
|
69
|
-
instrumentor = instrumentor_module.SQLAlchemyInstrumentor()
|
|
70
|
-
|
|
71
|
-
options = commenter_options or _DEFAULT_COMMENTER_OPTIONS
|
|
72
|
-
|
|
73
|
-
instrumentor.instrument(
|
|
74
|
-
engine=engine,
|
|
75
|
-
enable_commenter=enable_commenter,
|
|
76
|
-
commenter_options=options,
|
|
77
|
-
)
|
|
78
|
-
_LOGGER.info(
|
|
79
|
-
"db_tracing.instrumented",
|
|
80
|
-
extra={
|
|
81
|
-
"event": {
|
|
82
|
-
"engine_url": str(engine.url),
|
|
83
|
-
"commenter_enabled": enable_commenter,
|
|
84
|
-
},
|
|
85
|
-
"_skip_enrichers": True,
|
|
86
|
-
},
|
|
87
|
-
)
|
|
88
75
|
except RuntimeError as exc:
|
|
89
76
|
raise RuntimeError(
|
|
90
77
|
"SQLAlchemy instrumentation requires "
|
|
91
78
|
f"`pip install {_PACKAGE_HINT}`"
|
|
92
79
|
) from exc
|
|
80
|
+
instrumentor = instrumentor_module.SQLAlchemyInstrumentor()
|
|
81
|
+
|
|
82
|
+
if not any(known is engine for known in _ENGINES):
|
|
83
|
+
_ENGINES.append(engine)
|
|
84
|
+
if instrumentor.is_instrumented_by_opentelemetry:
|
|
85
|
+
instrumentor.uninstrument()
|
|
86
|
+
instrumentor.instrument(
|
|
87
|
+
engines=list(_ENGINES),
|
|
88
|
+
enable_commenter=enable_commenter,
|
|
89
|
+
commenter_options=commenter_options or _DEFAULT_COMMENTER_OPTIONS,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
event = {"engine_url": str(engine.url), "commenter_enabled": enable_commenter}
|
|
93
|
+
if not instrumentor.is_instrumented_by_opentelemetry:
|
|
94
|
+
# OpenTelemetry refuses unsupported SQLAlchemy versions and only logs it.
|
|
95
|
+
_LOGGER.warning(
|
|
96
|
+
"db_tracing.not_instrumented",
|
|
97
|
+
extra={"event": event, "_skip_enrichers": True},
|
|
98
|
+
)
|
|
99
|
+
return
|
|
100
|
+
_LOGGER.info(
|
|
101
|
+
"db_tracing.instrumented",
|
|
102
|
+
extra={"event": event, "_skip_enrichers": True},
|
|
103
|
+
)
|
|
93
104
|
|
|
94
105
|
|
|
95
106
|
def instrument_sqlalchemy_async(
|
|
@@ -134,6 +145,7 @@ def uninstrument_sqlalchemy() -> None:
|
|
|
134
145
|
instrumentor_module.SQLAlchemyInstrumentor().uninstrument()
|
|
135
146
|
except (ModuleNotFoundError, RuntimeError):
|
|
136
147
|
pass
|
|
148
|
+
_ENGINES.clear()
|
|
137
149
|
|
|
138
150
|
|
|
139
151
|
__all__ = [
|
|
@@ -146,6 +146,9 @@ def _setup_structured_logging(
|
|
|
146
146
|
security_policy: SecurityPolicy,
|
|
147
147
|
audit_key_provider: object | None,
|
|
148
148
|
) -> None:
|
|
149
|
+
# Hook the lifespan before install_otel_logs does, so on shutdown the log
|
|
150
|
+
# queue drains into the OTLP handler before its provider is shut down.
|
|
151
|
+
_register_logging_shutdown_hook(app)
|
|
149
152
|
otel_log_handler = None
|
|
150
153
|
logs_mode: Literal["local_json", "otlp", "both"] = "local_json"
|
|
151
154
|
if otel_logs_settings and otel_logs_settings.enabled:
|
|
@@ -172,7 +175,6 @@ def _setup_structured_logging(
|
|
|
172
175
|
extra_handlers=extra_handlers,
|
|
173
176
|
audit_key_provider=audit_key_provider,
|
|
174
177
|
)
|
|
175
|
-
_register_logging_shutdown_hook(app)
|
|
176
178
|
|
|
177
179
|
|
|
178
180
|
def _setup_metrics_backend(
|
{fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/logging/circuit_breaker.py
RENAMED
|
@@ -69,10 +69,22 @@ class SinkCircuitBreakerHandler(logging.Handler):
|
|
|
69
69
|
self._half_open_total = 0
|
|
70
70
|
self._closes_total = 0
|
|
71
71
|
|
|
72
|
+
# Stdlib handlers catch their own write errors and only report them
|
|
73
|
+
# through handleError(), so count those reports as failures too.
|
|
74
|
+
self._delegate_failed = False
|
|
75
|
+
report_error = delegate.handleError
|
|
76
|
+
|
|
77
|
+
def _count_and_report(record: logging.LogRecord) -> None:
|
|
78
|
+
self._delegate_failed = True
|
|
79
|
+
report_error(record)
|
|
80
|
+
|
|
81
|
+
delegate.handleError = _count_and_report # type: ignore[method-assign]
|
|
82
|
+
|
|
72
83
|
def emit(self, record: logging.LogRecord) -> None:
|
|
73
84
|
if self._should_skip():
|
|
74
85
|
return
|
|
75
86
|
|
|
87
|
+
self._delegate_failed = False
|
|
76
88
|
try:
|
|
77
89
|
self._delegate.handle(record)
|
|
78
90
|
except Exception:
|
|
@@ -80,7 +92,10 @@ class SinkCircuitBreakerHandler(logging.Handler):
|
|
|
80
92
|
self.handleError(record)
|
|
81
93
|
return
|
|
82
94
|
|
|
83
|
-
self.
|
|
95
|
+
if self._delegate_failed:
|
|
96
|
+
self._record_failure()
|
|
97
|
+
else:
|
|
98
|
+
self._record_success()
|
|
84
99
|
|
|
85
100
|
def setFormatter(self, fmt: logging.Formatter | None) -> None: # noqa: N802
|
|
86
101
|
super().setFormatter(fmt)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Callable, Protocol
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI
|
|
7
|
+
|
|
8
|
+
from ..config import MetricsFormat
|
|
9
|
+
from .prometheus import (
|
|
10
|
+
PrometheusMetricsBackend,
|
|
11
|
+
_build_prometheus_metrics_backend,
|
|
12
|
+
mark_prometheus_process_dead,
|
|
13
|
+
mount_metrics_endpoint,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MetricsBackend(Protocol):
|
|
18
|
+
"""Interface for metrics recording backends."""
|
|
19
|
+
|
|
20
|
+
def observe(
|
|
21
|
+
self,
|
|
22
|
+
method: str,
|
|
23
|
+
path: str,
|
|
24
|
+
status_code: int,
|
|
25
|
+
duration_seconds: float,
|
|
26
|
+
) -> None: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MountableMetricsBackend(Protocol):
|
|
30
|
+
"""Optional extension for backends that can mount their endpoint."""
|
|
31
|
+
|
|
32
|
+
def mount_endpoint(
|
|
33
|
+
self,
|
|
34
|
+
app: FastAPI,
|
|
35
|
+
*,
|
|
36
|
+
path: str = "/metrics",
|
|
37
|
+
metrics_format: MetricsFormat = "negotiate",
|
|
38
|
+
) -> None: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
MetricsBackendFactory = Callable[..., MetricsBackend]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class NoopMetricsBackend:
|
|
45
|
+
"""No-op backend that silently discards all observations."""
|
|
46
|
+
|
|
47
|
+
def observe(
|
|
48
|
+
self,
|
|
49
|
+
method: str,
|
|
50
|
+
path: str,
|
|
51
|
+
status_code: int,
|
|
52
|
+
duration_seconds: float,
|
|
53
|
+
) -> None:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_UUID_RE = re.compile(
|
|
58
|
+
r"/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}"
|
|
59
|
+
)
|
|
60
|
+
_NUMBER_RE = re.compile(r"/\d+")
|
|
61
|
+
_HEX_RE = re.compile(r"/[0-9a-fA-F]{16,}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def collapse_dynamic_segments(path: str) -> str:
|
|
65
|
+
"""Replace dynamic path segments with ``/:id`` to control label cardinality."""
|
|
66
|
+
normalized = _UUID_RE.sub("/:id", path)
|
|
67
|
+
normalized = _HEX_RE.sub("/:id", normalized)
|
|
68
|
+
return _NUMBER_RE.sub("/:id", normalized)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_BACKEND_FACTORIES: dict[str, MetricsBackendFactory] = {}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def register_metrics_backend(name: str, factory: MetricsBackendFactory) -> None:
|
|
75
|
+
normalized_name = name.strip().lower()
|
|
76
|
+
if not normalized_name:
|
|
77
|
+
raise ValueError("Metrics backend name cannot be empty")
|
|
78
|
+
_BACKEND_FACTORIES[normalized_name] = factory
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def unregister_metrics_backend(name: str) -> None:
|
|
82
|
+
_BACKEND_FACTORIES.pop(name.strip().lower(), None)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_registered_metrics_backends() -> dict[str, MetricsBackendFactory]:
|
|
86
|
+
return dict(_BACKEND_FACTORIES)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def build_metrics_backend(
|
|
90
|
+
enabled: bool,
|
|
91
|
+
*,
|
|
92
|
+
service: str = "api",
|
|
93
|
+
environment: str = "development",
|
|
94
|
+
exemplars_enabled: bool = False,
|
|
95
|
+
backend: str = "prometheus",
|
|
96
|
+
) -> MetricsBackend:
|
|
97
|
+
"""Factory for selecting the appropriate metrics backend."""
|
|
98
|
+
if not enabled:
|
|
99
|
+
return NoopMetricsBackend()
|
|
100
|
+
factory = _BACKEND_FACTORIES.get(backend.strip().lower())
|
|
101
|
+
if factory is None:
|
|
102
|
+
available_csv = ", ".join(sorted(_BACKEND_FACTORIES)) or "(none)"
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"Unknown metrics backend: {backend!r}. Available backends: {available_csv}"
|
|
105
|
+
)
|
|
106
|
+
return factory(
|
|
107
|
+
service=service,
|
|
108
|
+
environment=environment,
|
|
109
|
+
exemplars_enabled=exemplars_enabled,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def mount_backend_metrics_endpoint(
|
|
114
|
+
app: FastAPI,
|
|
115
|
+
backend: MetricsBackend,
|
|
116
|
+
*,
|
|
117
|
+
path: str = "/metrics",
|
|
118
|
+
metrics_format: MetricsFormat = "negotiate",
|
|
119
|
+
) -> bool:
|
|
120
|
+
mount_endpoint = getattr(backend, "mount_endpoint", None)
|
|
121
|
+
if not callable(mount_endpoint):
|
|
122
|
+
return False
|
|
123
|
+
mount_endpoint(app, path=path, metrics_format=metrics_format)
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
register_metrics_backend("prometheus", _build_prometheus_metrics_backend)
|
|
128
|
+
|
|
129
|
+
__all__ = [
|
|
130
|
+
"MetricsBackend",
|
|
131
|
+
"MountableMetricsBackend",
|
|
132
|
+
"MetricsBackendFactory",
|
|
133
|
+
"MetricsFormat",
|
|
134
|
+
"NoopMetricsBackend",
|
|
135
|
+
"collapse_dynamic_segments",
|
|
136
|
+
"register_metrics_backend",
|
|
137
|
+
"unregister_metrics_backend",
|
|
138
|
+
"get_registered_metrics_backends",
|
|
139
|
+
"build_metrics_backend",
|
|
140
|
+
"mount_backend_metrics_endpoint",
|
|
141
|
+
"PrometheusMetricsBackend",
|
|
142
|
+
"mark_prometheus_process_dead",
|
|
143
|
+
"mount_metrics_endpoint",
|
|
144
|
+
]
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Prometheus backend, ``/metrics`` endpoint and multiprocess helpers.
|
|
2
|
+
|
|
3
|
+
``prometheus_client`` is an optional dependency, so it is imported lazily.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import gzip
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import threading
|
|
12
|
+
from typing import Any, Callable, Iterator
|
|
13
|
+
|
|
14
|
+
from fastapi import FastAPI
|
|
15
|
+
from starlette.requests import Request
|
|
16
|
+
from starlette.responses import Response
|
|
17
|
+
from starlette.routing import Route
|
|
18
|
+
|
|
19
|
+
from ..config import MetricsFormat
|
|
20
|
+
from ..utils import normalize_path
|
|
21
|
+
|
|
22
|
+
_LOGGER = logging.getLogger("fastapiobserver.metrics")
|
|
23
|
+
_HTTP_LABELS = ("service", "environment", "method", "path", "status_code")
|
|
24
|
+
# Accept header pinned for the non-negotiating formats; prometheus_client picks the encoder.
|
|
25
|
+
_FORCED_ACCEPT = {
|
|
26
|
+
"openmetrics": "application/openmetrics-text",
|
|
27
|
+
"prometheus": "text/plain",
|
|
28
|
+
}
|
|
29
|
+
_SINK_COUNTERS = {
|
|
30
|
+
"failures_total": "Total sink handler failures observed by circuit breakers.",
|
|
31
|
+
"skipped_total": "Total records skipped while sink circuit breaker was open.",
|
|
32
|
+
"opens_total": "Total transitions into open state for sink circuit breakers.",
|
|
33
|
+
"half_open_total": "Total transitions into half-open state for sink circuit breakers.",
|
|
34
|
+
"closes_total": "Total transitions back to closed state for sink circuit breakers.",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _import_prometheus_client() -> Any:
|
|
39
|
+
try:
|
|
40
|
+
import prometheus_client
|
|
41
|
+
except ModuleNotFoundError as exc:
|
|
42
|
+
raise RuntimeError(
|
|
43
|
+
"Prometheus support requires `pip install fastapi-observer[prometheus]`"
|
|
44
|
+
) from exc
|
|
45
|
+
return prometheus_client
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _multiprocess_dir() -> str:
|
|
49
|
+
return os.getenv("PROMETHEUS_MULTIPROC_DIR", "").strip()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _validate_multiprocess_dir() -> None:
|
|
53
|
+
path = _multiprocess_dir()
|
|
54
|
+
if path and not (os.path.isdir(path) and os.access(path, os.W_OK)):
|
|
55
|
+
raise RuntimeError(
|
|
56
|
+
f"PROMETHEUS_MULTIPROC_DIR={path!r} must be an existing, writable directory. "
|
|
57
|
+
"Create it before starting workers."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def mark_prometheus_process_dead(pid: int) -> None:
|
|
62
|
+
"""Clean up a dead worker's files in multiprocess mode (gunicorn ``child_exit``)."""
|
|
63
|
+
if not _multiprocess_dir():
|
|
64
|
+
return
|
|
65
|
+
_import_prometheus_client()
|
|
66
|
+
from prometheus_client import multiprocess
|
|
67
|
+
|
|
68
|
+
multiprocess.mark_process_dead(pid)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _get_trace_exemplar() -> dict[str, str] | None:
|
|
72
|
+
"""Current OTel trace ID as a Prometheus exemplar, or ``None`` without a valid span."""
|
|
73
|
+
try:
|
|
74
|
+
from opentelemetry import trace as otel_trace
|
|
75
|
+
|
|
76
|
+
ctx = otel_trace.get_current_span().get_span_context()
|
|
77
|
+
if ctx and ctx.is_valid:
|
|
78
|
+
return {"TraceID": f"{ctx.trace_id:032x}"}
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class _LogQueueMetricsCollector:
|
|
85
|
+
"""Exposes log-queue, Logtail DLQ and sink circuit-breaker stats at scrape time."""
|
|
86
|
+
|
|
87
|
+
def collect(self) -> Iterator[Any]:
|
|
88
|
+
from prometheus_client.core import CounterMetricFamily, GaugeMetricFamily
|
|
89
|
+
|
|
90
|
+
from ..logging import get_log_queue_stats, get_sink_circuit_breaker_stats
|
|
91
|
+
from ..sinks import get_logtail_dlq_stats
|
|
92
|
+
|
|
93
|
+
queue = get_log_queue_stats()
|
|
94
|
+
yield GaugeMetricFamily(
|
|
95
|
+
"fastapiobserver_log_queue_size",
|
|
96
|
+
"Current number of log records waiting in the core queue.",
|
|
97
|
+
value=float(queue["queue_size"]),
|
|
98
|
+
)
|
|
99
|
+
yield GaugeMetricFamily(
|
|
100
|
+
"fastapiobserver_log_queue_capacity",
|
|
101
|
+
"Configured capacity of the core logging queue.",
|
|
102
|
+
value=float(queue["queue_capacity"]),
|
|
103
|
+
)
|
|
104
|
+
policy = GaugeMetricFamily(
|
|
105
|
+
"fastapiobserver_log_queue_overflow_policy_info",
|
|
106
|
+
"Current overflow policy for the core logging queue.",
|
|
107
|
+
labels=["policy"],
|
|
108
|
+
)
|
|
109
|
+
policy.add_metric([str(queue["overflow_policy"])], 1.0)
|
|
110
|
+
yield policy
|
|
111
|
+
yield CounterMetricFamily(
|
|
112
|
+
"fastapiobserver_log_queue_enqueued_total",
|
|
113
|
+
"Total log records accepted into the core queue.",
|
|
114
|
+
value=float(queue["enqueued_total"]),
|
|
115
|
+
)
|
|
116
|
+
dropped = CounterMetricFamily(
|
|
117
|
+
"fastapiobserver_log_queue_dropped_total",
|
|
118
|
+
"Total log records dropped due to queue pressure.",
|
|
119
|
+
labels=["reason"],
|
|
120
|
+
)
|
|
121
|
+
dropped.add_metric(["drop_oldest"], float(queue["dropped_oldest_total"]))
|
|
122
|
+
dropped.add_metric(["drop_newest"], float(queue["dropped_newest_total"]))
|
|
123
|
+
yield dropped
|
|
124
|
+
yield CounterMetricFamily(
|
|
125
|
+
"fastapiobserver_log_queue_blocked_total",
|
|
126
|
+
"Total times producers entered blocking mode while queue was full.",
|
|
127
|
+
value=float(queue["blocked_total"]),
|
|
128
|
+
)
|
|
129
|
+
yield CounterMetricFamily(
|
|
130
|
+
"fastapiobserver_log_queue_block_timeouts_total",
|
|
131
|
+
"Total blocking enqueue attempts that timed out and dropped the newest record.",
|
|
132
|
+
value=float(queue["block_timeout_total"]),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
dlq = get_logtail_dlq_stats()
|
|
136
|
+
written = CounterMetricFamily(
|
|
137
|
+
"fastapiobserver_dlq_written_total",
|
|
138
|
+
"Total payloads written to local DLQ.",
|
|
139
|
+
labels=["reason"],
|
|
140
|
+
)
|
|
141
|
+
written.add_metric(["queue_overflow"], float(dlq["written_queue_overflow"]))
|
|
142
|
+
written.add_metric(["send_failed"], float(dlq["written_send_failed"]))
|
|
143
|
+
yield written
|
|
144
|
+
yield CounterMetricFamily(
|
|
145
|
+
"fastapiobserver_dlq_write_failures_total",
|
|
146
|
+
"Total failures when attempting to write payloads to the DLQ disk.",
|
|
147
|
+
value=float(dlq["failures"]),
|
|
148
|
+
)
|
|
149
|
+
yield CounterMetricFamily(
|
|
150
|
+
"fastapiobserver_dlq_bytes_total",
|
|
151
|
+
"Total bytes written to the DLQ disk.",
|
|
152
|
+
value=float(dlq["bytes"]),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
sinks = sorted(get_sink_circuit_breaker_stats().items())
|
|
156
|
+
if not sinks:
|
|
157
|
+
return
|
|
158
|
+
state = GaugeMetricFamily(
|
|
159
|
+
"fastapiobserver_sink_circuit_breaker_state_info",
|
|
160
|
+
"Current sink circuit-breaker state.",
|
|
161
|
+
labels=["sink", "state"],
|
|
162
|
+
)
|
|
163
|
+
for sink_name, sink in sinks:
|
|
164
|
+
state.add_metric([sink_name, str(sink["state"])], 1.0)
|
|
165
|
+
yield state
|
|
166
|
+
for key, documentation in _SINK_COUNTERS.items():
|
|
167
|
+
counter = CounterMetricFamily(
|
|
168
|
+
f"fastapiobserver_sink_circuit_breaker_{key}", documentation, labels=["sink"]
|
|
169
|
+
)
|
|
170
|
+
for sink_name, sink in sinks:
|
|
171
|
+
counter.add_metric([sink_name], float(sink[key]))
|
|
172
|
+
yield counter
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _register_log_queue_metrics_collector() -> None:
|
|
176
|
+
# Per-worker queue stats would be meaningless once aggregated across processes.
|
|
177
|
+
if _multiprocess_dir():
|
|
178
|
+
return
|
|
179
|
+
try:
|
|
180
|
+
_import_prometheus_client().REGISTRY.register(_LogQueueMetricsCollector())
|
|
181
|
+
except ValueError:
|
|
182
|
+
pass # Already registered by an earlier backend build in this process.
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class PrometheusMetricsBackend:
|
|
186
|
+
"""Prometheus request counter and latency histogram.
|
|
187
|
+
|
|
188
|
+
With *exemplars_enabled*, observations carry the current OTel trace ID as an
|
|
189
|
+
exemplar (visible in OpenMetrics output), enabling the metrics -> traces jump
|
|
190
|
+
in Grafana. Exemplars are not supported by the multiprocess collector
|
|
191
|
+
(``PROMETHEUS_MULTIPROC_DIR``), so they are disabled there with a warning.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
_LOCK = threading.Lock()
|
|
195
|
+
_REQUEST_COUNT: Any = None
|
|
196
|
+
_REQUEST_LATENCY: Any = None
|
|
197
|
+
|
|
198
|
+
def __init__(
|
|
199
|
+
self,
|
|
200
|
+
*,
|
|
201
|
+
service: str,
|
|
202
|
+
environment: str,
|
|
203
|
+
exemplars_enabled: bool = False,
|
|
204
|
+
) -> None:
|
|
205
|
+
self.service = service
|
|
206
|
+
self.environment = environment
|
|
207
|
+
self._exemplars_enabled = exemplars_enabled
|
|
208
|
+
|
|
209
|
+
if self._exemplars_enabled and _multiprocess_dir():
|
|
210
|
+
_LOGGER.warning(
|
|
211
|
+
"metrics.exemplars.disabled_in_multiprocess",
|
|
212
|
+
extra={
|
|
213
|
+
"event": {
|
|
214
|
+
"message": (
|
|
215
|
+
"Prometheus exemplars are not compatible with "
|
|
216
|
+
"multiprocess mode. Exemplars have been disabled."
|
|
217
|
+
),
|
|
218
|
+
},
|
|
219
|
+
"_skip_enrichers": True,
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
self._exemplars_enabled = False
|
|
223
|
+
|
|
224
|
+
prometheus_client = _import_prometheus_client()
|
|
225
|
+
cls = PrometheusMetricsBackend
|
|
226
|
+
with cls._LOCK:
|
|
227
|
+
if cls._REQUEST_COUNT is None:
|
|
228
|
+
cls._REQUEST_COUNT = prometheus_client.Counter(
|
|
229
|
+
"http_requests_total",
|
|
230
|
+
"Total count of HTTP requests",
|
|
231
|
+
_HTTP_LABELS,
|
|
232
|
+
)
|
|
233
|
+
cls._REQUEST_LATENCY = prometheus_client.Histogram(
|
|
234
|
+
"http_request_duration_seconds",
|
|
235
|
+
"HTTP request latency",
|
|
236
|
+
_HTTP_LABELS,
|
|
237
|
+
buckets=(0.005, 0.01, 0.025, 0.05, 0.1, 0.3, 0.5, 1, 3, 5, 10),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def observe(
|
|
241
|
+
self,
|
|
242
|
+
method: str,
|
|
243
|
+
path: str,
|
|
244
|
+
status_code: int,
|
|
245
|
+
duration_seconds: float,
|
|
246
|
+
) -> None:
|
|
247
|
+
exemplar = _get_trace_exemplar() if self._exemplars_enabled else None
|
|
248
|
+
labels = (self.service, self.environment, method, path, str(status_code))
|
|
249
|
+
self._REQUEST_COUNT.labels(*labels).inc(exemplar=exemplar)
|
|
250
|
+
self._REQUEST_LATENCY.labels(*labels).observe(duration_seconds, exemplar=exemplar)
|
|
251
|
+
|
|
252
|
+
def mount_endpoint(
|
|
253
|
+
self,
|
|
254
|
+
app: FastAPI,
|
|
255
|
+
*,
|
|
256
|
+
path: str = "/metrics",
|
|
257
|
+
metrics_format: MetricsFormat = "negotiate",
|
|
258
|
+
) -> None:
|
|
259
|
+
mount_metrics_endpoint(app, path=path, metrics_format=metrics_format)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _build_prometheus_metrics_backend(
|
|
263
|
+
*,
|
|
264
|
+
service: str,
|
|
265
|
+
environment: str,
|
|
266
|
+
exemplars_enabled: bool,
|
|
267
|
+
) -> PrometheusMetricsBackend:
|
|
268
|
+
_validate_multiprocess_dir()
|
|
269
|
+
backend = PrometheusMetricsBackend(
|
|
270
|
+
service=service,
|
|
271
|
+
environment=environment,
|
|
272
|
+
exemplars_enabled=exemplars_enabled,
|
|
273
|
+
)
|
|
274
|
+
_register_log_queue_metrics_collector()
|
|
275
|
+
return backend
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _metrics_endpoint(registry: Any, accept: str | None) -> Callable[[Request], Response]:
|
|
279
|
+
"""Serve *registry* the way ``prometheus_client``'s own apps do.
|
|
280
|
+
|
|
281
|
+
A plain function, so Starlette runs it in its thread pool: rendering takes
|
|
282
|
+
tens to hundreds of milliseconds on large apps and must not block the event
|
|
283
|
+
loop, which ``make_asgi_app`` does. A pinned *accept* overrides negotiation.
|
|
284
|
+
"""
|
|
285
|
+
_import_prometheus_client()
|
|
286
|
+
from prometheus_client.exposition import choose_encoder, gzip_accepted
|
|
287
|
+
|
|
288
|
+
def metrics(request: Request) -> Response:
|
|
289
|
+
encoder, content_type = choose_encoder(accept or request.headers.get("accept", ""))
|
|
290
|
+
names = request.query_params.getlist("name[]")
|
|
291
|
+
body = encoder(registry.restricted_registry(names) if names else registry)
|
|
292
|
+
headers = {"Content-Type": content_type}
|
|
293
|
+
if gzip_accepted(request.headers.get("accept-encoding", "")):
|
|
294
|
+
body = gzip.compress(body)
|
|
295
|
+
headers["Content-Encoding"] = "gzip"
|
|
296
|
+
return Response(body, headers=headers)
|
|
297
|
+
|
|
298
|
+
return metrics
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def mount_metrics_endpoint(
|
|
302
|
+
app: FastAPI,
|
|
303
|
+
path: str = "/metrics",
|
|
304
|
+
*,
|
|
305
|
+
metrics_format: MetricsFormat = "negotiate",
|
|
306
|
+
) -> None:
|
|
307
|
+
"""Serve Prometheus metrics at *path* (GET/HEAD, gzip when accepted).
|
|
308
|
+
|
|
309
|
+
* ``"negotiate"`` — ``prometheus_client`` picks OpenMetrics or classic text
|
|
310
|
+
from the ``Accept`` header.
|
|
311
|
+
* ``"openmetrics"`` — always OpenMetrics (exemplars visible).
|
|
312
|
+
* ``"prometheus"`` — always classic Prometheus text format.
|
|
313
|
+
"""
|
|
314
|
+
path = normalize_path(path, default="/metrics")
|
|
315
|
+
if any(getattr(route, "path", None) == path for route in app.routes):
|
|
316
|
+
return
|
|
317
|
+
prometheus_client = _import_prometheus_client()
|
|
318
|
+
registry = prometheus_client.REGISTRY
|
|
319
|
+
if _multiprocess_dir():
|
|
320
|
+
from prometheus_client import multiprocess
|
|
321
|
+
|
|
322
|
+
registry = prometheus_client.CollectorRegistry(auto_describe=True)
|
|
323
|
+
multiprocess.MultiProcessCollector(registry)
|
|
324
|
+
endpoint = _metrics_endpoint(registry, _FORCED_ACCEPT.get(metrics_format))
|
|
325
|
+
app.routes.insert(0, Route(path, endpoint=endpoint, methods=["GET"]))
|
{fastapi_observer-1.6.0 → fastapi_observer-1.7.0}/src/fastapiobserver/middleware/__init__.py
RENAMED
|
@@ -7,7 +7,6 @@ from .headers import _get_header, _upsert_header
|
|
|
7
7
|
from .ip import _IpResolver, _extract_scope_client_ip
|
|
8
8
|
from .metrics import _MetricsRecorder
|
|
9
9
|
from .request_logging import RequestLoggingMiddleware
|
|
10
|
-
from .span_errors import _SpanErrorRecorder
|
|
11
10
|
|
|
12
11
|
__all__ = [
|
|
13
12
|
"RequestLoggingMiddleware",
|
|
@@ -16,7 +15,6 @@ __all__ = [
|
|
|
16
15
|
"_MetricsRecorder",
|
|
17
16
|
"_RequestContextManager",
|
|
18
17
|
"_RequestEventBuilder",
|
|
19
|
-
"_SpanErrorRecorder",
|
|
20
18
|
"_classify_error",
|
|
21
19
|
"_extract_route_template",
|
|
22
20
|
"_extract_scope_client_ip",
|