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.
@@ -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.4
1
+ Metadata-Version: 2.5
2
2
  Name: hostess-python
3
- Version: 0.1.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.1.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