hostess-python 0.1.0__tar.gz → 0.2.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.
- {hostess_python-0.1.0 → hostess_python-0.2.0}/CHANGELOG.md +24 -0
- {hostess_python-0.1.0 → hostess_python-0.2.0}/PKG-INFO +3 -2
- {hostess_python-0.1.0 → hostess_python-0.2.0}/pyproject.toml +5 -1
- {hostess_python-0.1.0 → hostess_python-0.2.0}/src/hostess_sdk/fastapi.py +38 -4
- hostess_python-0.2.0/tests/test_fastapi_post_instrument.py +178 -0
- {hostess_python-0.1.0 → hostess_python-0.2.0}/.github/workflows/ci.yml +0 -0
- {hostess_python-0.1.0 → hostess_python-0.2.0}/.github/workflows/publish.yml +0 -0
- {hostess_python-0.1.0 → hostess_python-0.2.0}/.gitignore +0 -0
- {hostess_python-0.1.0 → hostess_python-0.2.0}/.python-version +0 -0
- {hostess_python-0.1.0 → hostess_python-0.2.0}/LICENSE +0 -0
- {hostess_python-0.1.0 → hostess_python-0.2.0}/README.md +0 -0
- {hostess_python-0.1.0 → hostess_python-0.2.0}/examples/fastapi-basic/main.py +0 -0
- {hostess_python-0.1.0 → hostess_python-0.2.0}/src/hostess_sdk/__init__.py +0 -0
- {hostess_python-0.1.0 → hostess_python-0.2.0}/src/hostess_sdk/_otel/__init__.py +0 -0
- {hostess_python-0.1.0 → hostess_python-0.2.0}/src/hostess_sdk/_otel/marker.py +0 -0
- {hostess_python-0.1.0 → hostess_python-0.2.0}/tests/test_fastapi_instrumentation.py +0 -0
- {hostess_python-0.1.0 → hostess_python-0.2.0}/tests/test_fastapi_marker.py +0 -0
|
@@ -5,6 +5,29 @@ All notable changes to this project are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.0] - 2026-08-24
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `hostess_sdk.fastapi.post_instrument()` — additive OpenTelemetry
|
|
13
|
+
post-instrument hook for Hostess-owned telemetry. Starts the marker
|
|
14
|
+
heartbeat after `opentelemetry-instrument` has already instrumented
|
|
15
|
+
FastAPI. Does not replace the global `TracerProvider` or install FastAPI
|
|
16
|
+
instrumentation.
|
|
17
|
+
- `opentelemetry_post_instrument` entry point (`hostess_sdk`) so
|
|
18
|
+
`opentelemetry-instrument fastapi run` discovers the hook automatically.
|
|
19
|
+
- `opentelemetry-distro` added to the `fastapi` extra so
|
|
20
|
+
`opentelemetry-instrument` can load the default distro and discover
|
|
21
|
+
post-instrument entry points. No Hostess `opentelemetry_configurator`
|
|
22
|
+
is registered.
|
|
23
|
+
|
|
24
|
+
### Behavior
|
|
25
|
+
|
|
26
|
+
- `post_instrument()` and `instrument(app)` are mutually idempotent: if
|
|
27
|
+
both run, the marker heartbeat starts once.
|
|
28
|
+
- `post_instrument()` is a no-op when disabled (`HOSTESS_INSTRUMENTATION=false`)
|
|
29
|
+
or when no collector endpoint is available.
|
|
30
|
+
|
|
8
31
|
## [0.1.0] - 2026-06-13
|
|
9
32
|
|
|
10
33
|
Initial release. FastAPI integration for Hostess **API Insights**.
|
|
@@ -33,4 +56,5 @@ Initial release. FastAPI integration for Hostess **API Insights**.
|
|
|
33
56
|
`HOSTESS_INSTRUMENTATION=false`) or when no collector endpoint is available.
|
|
34
57
|
- Fails silent and cheap — never adds request latency or crashes the host app.
|
|
35
58
|
|
|
59
|
+
[0.2.0]: https://github.com/howl-cloud/hostess-python/releases/tag/v0.2.0
|
|
36
60
|
[0.1.0]: https://github.com/howl-cloud/hostess-python/releases/tag/v0.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: hostess-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Hostess SDK for Python — native API Insights for FastAPI and beyond.
|
|
5
5
|
Project-URL: Homepage, https://hostess.sh
|
|
6
6
|
Project-URL: Repository, https://github.com/howl-cloud/hostess-python
|
|
@@ -36,6 +36,7 @@ Requires-Python: >=3.9
|
|
|
36
36
|
Requires-Dist: opentelemetry-api>=1.27.0
|
|
37
37
|
Requires-Dist: opentelemetry-sdk>=1.27.0
|
|
38
38
|
Provides-Extra: fastapi
|
|
39
|
+
Requires-Dist: opentelemetry-distro>=0.48b0; extra == 'fastapi'
|
|
39
40
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27.0; extra == 'fastapi'
|
|
40
41
|
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.48b0; extra == 'fastapi'
|
|
41
42
|
Description-Content-Type: text/markdown
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hostess-python"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Hostess SDK for Python — native API Insights for FastAPI and beyond."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -28,8 +28,12 @@ dependencies = [
|
|
|
28
28
|
fastapi = [
|
|
29
29
|
"opentelemetry-instrumentation-fastapi>=0.48b0",
|
|
30
30
|
"opentelemetry-exporter-otlp-proto-http>=1.27.0",
|
|
31
|
+
"opentelemetry-distro>=0.48b0",
|
|
31
32
|
]
|
|
32
33
|
|
|
34
|
+
[project.entry-points.opentelemetry_post_instrument]
|
|
35
|
+
hostess_sdk = "hostess_sdk.fastapi:post_instrument"
|
|
36
|
+
|
|
33
37
|
[project.urls]
|
|
34
38
|
Homepage = "https://hostess.sh"
|
|
35
39
|
Repository = "https://github.com/howl-cloud/hostess-python"
|
|
@@ -7,10 +7,6 @@ Usage::
|
|
|
7
7
|
|
|
8
8
|
app = FastAPI()
|
|
9
9
|
instrument(app)
|
|
10
|
-
|
|
11
|
-
The helper is deliberately thin: it preconfigures battle-tested OpenTelemetry
|
|
12
|
-
FastAPI instrumentation to push to the Hostess collector, and emits the marker
|
|
13
|
-
heartbeat. It adds no routes to the application.
|
|
14
10
|
"""
|
|
15
11
|
|
|
16
12
|
from __future__ import annotations
|
|
@@ -98,6 +94,44 @@ def instrument(
|
|
|
98
94
|
return app
|
|
99
95
|
|
|
100
96
|
|
|
97
|
+
def post_instrument() -> None:
|
|
98
|
+
"""Hostess-specific init after ``opentelemetry-instrument`` has run.
|
|
99
|
+
|
|
100
|
+
Additive: starts the marker heartbeat. Does not install FastAPI
|
|
101
|
+
instrumentation or replace the global TracerProvider. Idempotent with
|
|
102
|
+
``instrument(app)`` — if both run, the marker starts once. Fail-silent
|
|
103
|
+
and a no-op when disabled or when no collector endpoint is resolved.
|
|
104
|
+
"""
|
|
105
|
+
if not is_enabled(None):
|
|
106
|
+
logger.debug("hostess: instrumentation disabled; skipping post-instrument")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
endpoint = resolve_endpoint(None)
|
|
110
|
+
if endpoint is None:
|
|
111
|
+
logger.debug("hostess: no OTLP endpoint resolved; post-instrument is a no-op")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
sdk_version, framework_version = _versions()
|
|
115
|
+
resource = build_resource(
|
|
116
|
+
service_name=None,
|
|
117
|
+
sdk_version=sdk_version,
|
|
118
|
+
framework=_FRAMEWORK,
|
|
119
|
+
framework_version=framework_version,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
start_marker_heartbeat(
|
|
124
|
+
resource=resource,
|
|
125
|
+
base_endpoint=endpoint,
|
|
126
|
+
language=_LANGUAGE,
|
|
127
|
+
framework=_FRAMEWORK,
|
|
128
|
+
sdk_version=sdk_version,
|
|
129
|
+
framework_version=framework_version,
|
|
130
|
+
)
|
|
131
|
+
except Exception: # never break the host app over telemetry setup
|
|
132
|
+
logger.debug("hostess: post-instrument setup failed; continuing", exc_info=True)
|
|
133
|
+
|
|
134
|
+
|
|
101
135
|
def _instrument_fastapi(app: "FastAPI", exclude_paths: Optional[Iterable[str]]) -> None:
|
|
102
136
|
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
103
137
|
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from importlib.metadata import entry_points
|
|
2
|
+
from inspect import signature
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
from opentelemetry.sdk.metrics.export import MetricExporter, MetricExportResult
|
|
7
|
+
|
|
8
|
+
from hostess_sdk import fastapi as hostess_fastapi
|
|
9
|
+
from hostess_sdk._otel import marker
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _entry_points(group: str):
|
|
13
|
+
try:
|
|
14
|
+
return entry_points(group=group)
|
|
15
|
+
except TypeError: # Python 3.9
|
|
16
|
+
return entry_points().get(group, [])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _FakeExporter(MetricExporter):
|
|
20
|
+
"""A real MetricExporter that exports nowhere (no network in tests)."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, *args, **kwargs):
|
|
23
|
+
super().__init__()
|
|
24
|
+
|
|
25
|
+
def export(self, metrics_data, timeout_millis=10_000, **kwargs):
|
|
26
|
+
return MetricExportResult.SUCCESS
|
|
27
|
+
|
|
28
|
+
def force_flush(self, timeout_millis=10_000):
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
def shutdown(self, timeout_millis=10_000, **kwargs):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture(autouse=True)
|
|
36
|
+
def _reset(monkeypatch):
|
|
37
|
+
hostess_fastapi._reset_for_tests()
|
|
38
|
+
marker._reset_for_tests()
|
|
39
|
+
for var in (
|
|
40
|
+
"HOSTESS_OTEL_ENDPOINT",
|
|
41
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
42
|
+
"HOSTESS_INSTRUMENTATION",
|
|
43
|
+
"HOSTESS_SERVICE_NAME",
|
|
44
|
+
):
|
|
45
|
+
monkeypatch.delenv(var, raising=False)
|
|
46
|
+
yield
|
|
47
|
+
hostess_fastapi._reset_for_tests()
|
|
48
|
+
marker._reset_for_tests()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_post_instrument_is_public_and_takes_no_args():
|
|
52
|
+
from hostess_sdk.fastapi import post_instrument
|
|
53
|
+
|
|
54
|
+
assert list(signature(post_instrument).parameters) == []
|
|
55
|
+
post_instrument() # no endpoint → no-op; must not raise
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_entry_point_registered():
|
|
59
|
+
eps = list(_entry_points("opentelemetry_post_instrument"))
|
|
60
|
+
match = [ep for ep in eps if ep.name == "hostess_sdk"]
|
|
61
|
+
assert match, "hostess_sdk missing from opentelemetry_post_instrument"
|
|
62
|
+
assert match[0].value == "hostess_sdk.fastapi:post_instrument"
|
|
63
|
+
assert match[0].load() is hostess_fastapi.post_instrument
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_no_hostess_configurator_entry_point():
|
|
67
|
+
values = [ep.value for ep in _entry_points("opentelemetry_configurator")]
|
|
68
|
+
assert not any(value.startswith("hostess_sdk") for value in values)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_post_instrument_noop_without_endpoint():
|
|
72
|
+
hostess_fastapi.post_instrument()
|
|
73
|
+
assert marker._marker_provider is None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_post_instrument_noop_when_disabled(monkeypatch):
|
|
77
|
+
monkeypatch.setenv("HOSTESS_OTEL_ENDPOINT", "http://hostess-otel:4318")
|
|
78
|
+
monkeypatch.setenv("HOSTESS_INSTRUMENTATION", "false")
|
|
79
|
+
hostess_fastapi.post_instrument()
|
|
80
|
+
assert marker._marker_provider is None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_post_instrument_starts_marker_once(monkeypatch):
|
|
84
|
+
monkeypatch.setenv("HOSTESS_OTEL_ENDPOINT", "http://hostess-otel:4318")
|
|
85
|
+
monkeypatch.setattr(
|
|
86
|
+
"opentelemetry.exporter.otlp.proto.http.metric_exporter.OTLPMetricExporter",
|
|
87
|
+
_FakeExporter,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
hostess_fastapi.post_instrument()
|
|
91
|
+
first = marker._marker_provider
|
|
92
|
+
assert first is not None
|
|
93
|
+
|
|
94
|
+
hostess_fastapi.post_instrument()
|
|
95
|
+
assert marker._marker_provider is first
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_instrument_then_post_instrument_starts_marker_once(monkeypatch):
|
|
99
|
+
monkeypatch.setenv("HOSTESS_OTEL_ENDPOINT", "http://hostess-otel:4318")
|
|
100
|
+
monkeypatch.setattr(
|
|
101
|
+
"opentelemetry.exporter.otlp.proto.http.metric_exporter.OTLPMetricExporter",
|
|
102
|
+
_FakeExporter,
|
|
103
|
+
)
|
|
104
|
+
monkeypatch.setattr(hostess_fastapi, "ensure_tracer_provider", lambda *a, **k: None)
|
|
105
|
+
monkeypatch.setattr(hostess_fastapi, "_instrument_fastapi", lambda *a, **k: None)
|
|
106
|
+
|
|
107
|
+
app = FastAPI()
|
|
108
|
+
hostess_fastapi.instrument(app)
|
|
109
|
+
first = marker._marker_provider
|
|
110
|
+
assert first is not None
|
|
111
|
+
|
|
112
|
+
hostess_fastapi.post_instrument()
|
|
113
|
+
assert marker._marker_provider is first
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_post_instrument_then_instrument_starts_marker_once(monkeypatch):
|
|
117
|
+
monkeypatch.setenv("HOSTESS_OTEL_ENDPOINT", "http://hostess-otel:4318")
|
|
118
|
+
monkeypatch.setattr(
|
|
119
|
+
"opentelemetry.exporter.otlp.proto.http.metric_exporter.OTLPMetricExporter",
|
|
120
|
+
_FakeExporter,
|
|
121
|
+
)
|
|
122
|
+
monkeypatch.setattr(hostess_fastapi, "ensure_tracer_provider", lambda *a, **k: None)
|
|
123
|
+
monkeypatch.setattr(hostess_fastapi, "_instrument_fastapi", lambda *a, **k: None)
|
|
124
|
+
|
|
125
|
+
hostess_fastapi.post_instrument()
|
|
126
|
+
first = marker._marker_provider
|
|
127
|
+
assert first is not None
|
|
128
|
+
|
|
129
|
+
hostess_fastapi.instrument(FastAPI())
|
|
130
|
+
assert marker._marker_provider is first
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_post_instrument_does_not_instrument_fastapi(monkeypatch):
|
|
134
|
+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
135
|
+
|
|
136
|
+
monkeypatch.setenv("HOSTESS_OTEL_ENDPOINT", "http://hostess-otel:4318")
|
|
137
|
+
monkeypatch.setattr(
|
|
138
|
+
"opentelemetry.exporter.otlp.proto.http.metric_exporter.OTLPMetricExporter",
|
|
139
|
+
_FakeExporter,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
called = []
|
|
143
|
+
monkeypatch.setattr(
|
|
144
|
+
FastAPIInstrumentor, "instrument_app", lambda *a, **k: called.append("app")
|
|
145
|
+
)
|
|
146
|
+
monkeypatch.setattr(
|
|
147
|
+
FastAPIInstrumentor, "instrument", lambda *a, **k: called.append("global")
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
hostess_fastapi.post_instrument()
|
|
151
|
+
assert called == []
|
|
152
|
+
assert marker._marker_provider is not None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_post_instrument_does_not_replace_tracer_provider(monkeypatch):
|
|
156
|
+
from opentelemetry import trace
|
|
157
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
158
|
+
|
|
159
|
+
monkeypatch.setenv("HOSTESS_OTEL_ENDPOINT", "http://hostess-otel:4318")
|
|
160
|
+
monkeypatch.setattr(
|
|
161
|
+
"opentelemetry.exporter.otlp.proto.http.metric_exporter.OTLPMetricExporter",
|
|
162
|
+
_FakeExporter,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
current = trace.get_tracer_provider()
|
|
166
|
+
if not isinstance(current, TracerProvider):
|
|
167
|
+
current = TracerProvider()
|
|
168
|
+
trace.set_tracer_provider(current)
|
|
169
|
+
installed = trace.get_tracer_provider()
|
|
170
|
+
|
|
171
|
+
def _boom(*_a, **_k):
|
|
172
|
+
raise AssertionError("post_instrument must not call set_tracer_provider")
|
|
173
|
+
|
|
174
|
+
monkeypatch.setattr(trace, "set_tracer_provider", _boom)
|
|
175
|
+
|
|
176
|
+
hostess_fastapi.post_instrument()
|
|
177
|
+
assert trace.get_tracer_provider() is installed
|
|
178
|
+
assert marker._marker_provider is not None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|