hostess-python 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: hostess-python
3
+ Version: 0.1.0
4
+ Summary: Hostess SDK for Python — native API Insights for FastAPI and beyond.
5
+ Project-URL: Homepage, https://hostess.sh
6
+ Project-URL: Repository, https://github.com/howl-cloud/hostess-python
7
+ Author: Horizon Web Labs
8
+ License: MIT License
9
+
10
+ Copyright (c) 2026 Horizon Web Labs
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+ License-File: LICENSE
30
+ Keywords: fastapi,hostess,insights,observability,opentelemetry
31
+ Classifier: Development Status :: 3 - Alpha
32
+ Classifier: Intended Audience :: Developers
33
+ Classifier: Programming Language :: Python :: 3
34
+ Classifier: Topic :: System :: Monitoring
35
+ Requires-Python: >=3.9
36
+ Requires-Dist: opentelemetry-api>=1.27.0
37
+ Requires-Dist: opentelemetry-sdk>=1.27.0
38
+ Provides-Extra: fastapi
39
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27.0; extra == 'fastapi'
40
+ Requires-Dist: opentelemetry-instrumentation-fastapi>=0.48b0; extra == 'fastapi'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # hostess-python
44
+
45
+ The Hostess SDK for Python. Native **API Insights** for FastAPI (and, later,
46
+ other Python frameworks) with one line of code.
47
+
48
+ ```bash
49
+ pip install "hostess-python[fastapi]"
50
+ ```
51
+
52
+ ```python
53
+ from fastapi import FastAPI
54
+ from hostess_sdk.fastapi import instrument
55
+
56
+ app = FastAPI()
57
+ instrument(app)
58
+ ```
59
+
60
+ That's it. When deployed on Hostess, route-level traffic, latency, status
61
+ codes, and error rates appear in Studio — no OTLP, collector, or Prometheus
62
+ configuration required.
63
+
64
+ ## What `instrument(app)` does
65
+
66
+ - Applies OpenTelemetry FastAPI instrumentation, so requests produce server
67
+ spans carrying the **route template** (`/items/{item_id}`), method, and
68
+ status — never raw paths or query strings.
69
+ - Exports spans over OTLP/HTTP to the Hostess collector. The endpoint is
70
+ injected by the platform (`HOSTESS_OTEL_ENDPOINT`); override with
71
+ `otlp_endpoint=` for local use.
72
+ - Emits a periodic **marker heartbeat** so Studio can tell "installed, waiting
73
+ for traffic" from "not installed".
74
+
75
+ It is idempotent, fails silent and cheap (bounded queue, never adds request
76
+ latency or crashes the app), and is a clean no-op when disabled
77
+ (`enabled=False` / `HOSTESS_INSTRUMENTATION=false`) or when no collector
78
+ endpoint is available.
79
+
80
+ ## Options
81
+
82
+ ```python
83
+ instrument(
84
+ app,
85
+ enabled=None, # None → env (HOSTESS_INSTRUMENTATION), default on
86
+ service_name=None, # defaults to the platform-injected name
87
+ exclude_paths=["/health"],
88
+ otlp_endpoint=None, # local/advanced override
89
+ )
90
+ ```
91
+
92
+ ## Privacy
93
+
94
+ The helper collects HTTP method, route template, status code, and duration. It
95
+ never collects request/response bodies, query strings, raw URLs, headers,
96
+ cookies, tokens, user IDs, or client IPs.
97
+
98
+ ## Development
99
+
100
+ ```bash
101
+ uv sync
102
+ uv run pytest
103
+ ```
104
+
105
+ ## Status
106
+
107
+ v0.1 — `instrument(app)` for FastAPI. Roadmap: a `HostessFastAPI` drop-in,
108
+ opt-in exception metrics, and additional framework helpers (Django, Flask).
@@ -0,0 +1,8 @@
1
+ hostess_sdk/__init__.py,sha256=MgnQ13cclXwFRPoVnzxVpqpVt6tMaTMlexwsAwz486Q,598
2
+ hostess_sdk/fastapi.py,sha256=NxLRQUH5K44WOB8Hhox-_VjDJxQsvHAIJZf2Xp2U7ZY,3782
3
+ hostess_sdk/_otel/__init__.py,sha256=9w2DP9j128BGfoDp_szyLaL65j5g55TrK-2u7f5YI0Y,3724
4
+ hostess_sdk/_otel/marker.py,sha256=qVFnlCLEIqeB4S-eUHOYbko8mmQ-gdE-TXTC-BWkOUE,2780
5
+ hostess_python-0.1.0.dist-info/METADATA,sha256=X9baUeG5rTmdGV0XleHVLuwp3ayhH3035EY83gq1-BM,4073
6
+ hostess_python-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
7
+ hostess_python-0.1.0.dist-info/licenses/LICENSE,sha256=eGflFTCTA2JY5LjhbvOqDh1cO_ZfjocMJ_L7KxNe7YU,1073
8
+ hostess_python-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Horizon Web Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,19 @@
1
+ """Hostess SDK for Python.
2
+
3
+ The public integration surface lives in framework submodules, e.g.::
4
+
5
+ from hostess_sdk.fastapi import instrument
6
+
7
+ The base package is intentionally dependency-light; framework helpers pull in
8
+ their instrumentation via extras (``hostess-python[fastapi]``).
9
+ """
10
+
11
+ from importlib.metadata import PackageNotFoundError
12
+ from importlib.metadata import version as _pkg_version
13
+
14
+ try:
15
+ __version__ = _pkg_version("hostess-python")
16
+ except PackageNotFoundError: # editable/source checkout without installed metadata
17
+ __version__ = "0.0.0+local"
18
+
19
+ __all__ = ["__version__"]
@@ -0,0 +1,108 @@
1
+ """Shared OTel setup: endpoint/enabled resolution, resource, tracer provider.
2
+
3
+ Tracing attaches to the app's existing global ``TracerProvider`` when one is
4
+ present (so the helper's spans share context with the app), and only installs
5
+ its own when none exists. The marker heartbeat (see ``marker.py``) uses a
6
+ *dedicated, non-global* ``MeterProvider`` instead, because metric readers cannot
7
+ be added to an existing provider and we must never clobber the app's metrics.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import os
14
+
15
+ from opentelemetry import trace
16
+ from opentelemetry.sdk.resources import Resource
17
+ from opentelemetry.sdk.trace import TracerProvider
18
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
19
+
20
+ logger = logging.getLogger("hostess_sdk")
21
+
22
+ _TRUE = {"1", "true", "yes", "on"}
23
+ _FALSE = {"0", "false", "no", "off"}
24
+
25
+
26
+ def is_enabled(enabled: bool | None) -> bool:
27
+ """Resolve the enabled flag: explicit arg, then env, then default-on."""
28
+ if enabled is not None:
29
+ return enabled
30
+ raw = os.getenv("HOSTESS_INSTRUMENTATION")
31
+ if raw is not None and raw.strip().lower() in _FALSE:
32
+ return False
33
+ return True
34
+
35
+
36
+ def resolve_endpoint(explicit: str | None) -> str | None:
37
+ """Resolve the OTLP base endpoint.
38
+
39
+ Precedence: platform-injected ``HOSTESS_OTEL_ENDPOINT`` →
40
+ ``OTEL_EXPORTER_OTLP_ENDPOINT`` → the explicit ``otlp_endpoint`` argument.
41
+ """
42
+ for candidate in (
43
+ os.getenv("HOSTESS_OTEL_ENDPOINT"),
44
+ os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"),
45
+ explicit,
46
+ ):
47
+ if candidate and candidate.strip():
48
+ return candidate.strip()
49
+ return None
50
+
51
+
52
+ def signal_endpoint(base: str, signal: str) -> str:
53
+ """Append the OTLP/HTTP signal path (``v1/traces``, ``v1/metrics``)."""
54
+ base = base.rstrip("/")
55
+ suffix = f"/v1/{signal}"
56
+ if base.endswith(suffix):
57
+ return base
58
+ return base + suffix
59
+
60
+
61
+ def build_resource(
62
+ *,
63
+ service_name: str | None,
64
+ sdk_version: str,
65
+ framework: str,
66
+ framework_version: str,
67
+ ) -> Resource:
68
+ """Build the OTel resource.
69
+
70
+ ``Resource.create`` already folds in env-detected attributes
71
+ (``OTEL_SERVICE_NAME``, ``OTEL_RESOURCE_ATTRIBUTES`` injected by the
72
+ platform). These are advisory: authoritative tenant attribution comes from
73
+ the collector's k8sattributes processor.
74
+ """
75
+ attrs: dict[str, str] = {
76
+ "hostess.sdk.language": "python",
77
+ "hostess.sdk.version": sdk_version,
78
+ "hostess.framework": framework,
79
+ "hostess.framework.version": framework_version,
80
+ }
81
+ resolved_name = service_name or os.getenv("HOSTESS_SERVICE_NAME")
82
+ if resolved_name:
83
+ attrs["service.name"] = resolved_name
84
+ return Resource.create(attrs)
85
+
86
+
87
+ def ensure_tracer_provider(resource: Resource, base_endpoint: str) -> TracerProvider:
88
+ """Return the global SDK ``TracerProvider``, attaching our OTLP exporter.
89
+
90
+ Attaches a span processor to an existing SDK provider, or installs a new
91
+ one (with our resource) if only the API default proxy is present.
92
+ """
93
+ # Imported lazily so the base package doesn't hard-require the http exporter
94
+ # (it ships with the framework extra).
95
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
96
+
97
+ exporter = OTLPSpanExporter(endpoint=signal_endpoint(base_endpoint, "traces"))
98
+ processor = BatchSpanProcessor(exporter)
99
+
100
+ current = trace.get_tracer_provider()
101
+ if isinstance(current, TracerProvider):
102
+ current.add_span_processor(processor)
103
+ return current
104
+
105
+ provider = TracerProvider(resource=resource)
106
+ provider.add_span_processor(processor)
107
+ trace.set_tracer_provider(provider)
108
+ return provider
@@ -0,0 +1,86 @@
1
+ """The instrumentation-info marker heartbeat.
2
+
3
+ Periodically exports the gauge
4
+ ``hostess_instrumentation_info{language,framework,sdk_version,framework_version} 1``.
5
+ This is what lets the platform tell "installed, no traffic yet" from "not
6
+ installed" — spans only appear once requests arrive.
7
+
8
+ Uses its own ``MeterProvider`` (not the global one) so it exports the marker to
9
+ the Hostess collector regardless of, and without disturbing, any MeterProvider
10
+ the application configures for its own metrics.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from typing import Iterable
17
+
18
+ from opentelemetry.metrics import CallbackOptions, Observation
19
+ from opentelemetry.sdk.metrics import MeterProvider
20
+ from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
21
+ from opentelemetry.sdk.resources import Resource
22
+
23
+ from . import signal_endpoint
24
+
25
+ logger = logging.getLogger("hostess_sdk")
26
+
27
+ # Holds the dedicated provider so its reader thread isn't garbage-collected and
28
+ # so a re-instrument call can detect it (idempotency).
29
+ _marker_provider: MeterProvider | None = None
30
+
31
+
32
+ def start_marker_heartbeat(
33
+ *,
34
+ resource: Resource,
35
+ base_endpoint: str,
36
+ language: str,
37
+ framework: str,
38
+ sdk_version: str,
39
+ framework_version: str,
40
+ interval_seconds: int = 60,
41
+ ) -> MeterProvider | None:
42
+ """Start the marker heartbeat. Idempotent: a no-op if already running."""
43
+ global _marker_provider
44
+ if _marker_provider is not None:
45
+ return _marker_provider
46
+
47
+ from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
48
+ OTLPMetricExporter,
49
+ )
50
+
51
+ exporter = OTLPMetricExporter(endpoint=signal_endpoint(base_endpoint, "metrics"))
52
+ reader = PeriodicExportingMetricReader(
53
+ exporter, export_interval_millis=interval_seconds * 1000
54
+ )
55
+ provider = MeterProvider(resource=resource, metric_readers=[reader])
56
+ meter = provider.get_meter("hostess_sdk")
57
+
58
+ attributes = {
59
+ "language": language,
60
+ "framework": framework,
61
+ "sdk_version": sdk_version,
62
+ "framework_version": framework_version,
63
+ }
64
+
65
+ def _observe(_options: CallbackOptions) -> Iterable[Observation]:
66
+ yield Observation(1, attributes)
67
+
68
+ meter.create_observable_gauge(
69
+ name="hostess_instrumentation_info",
70
+ callbacks=[_observe],
71
+ description="Hostess instrumentation marker (1 = installed).",
72
+ )
73
+
74
+ _marker_provider = provider
75
+ return provider
76
+
77
+
78
+ def _reset_for_tests() -> None:
79
+ """Tear down the heartbeat so tests can re-run instrument() cleanly."""
80
+ global _marker_provider
81
+ if _marker_provider is not None:
82
+ try:
83
+ _marker_provider.shutdown()
84
+ except Exception: # pragma: no cover - best effort
85
+ pass
86
+ _marker_provider = None
hostess_sdk/fastapi.py ADDED
@@ -0,0 +1,123 @@
1
+ """FastAPI integration for Hostess.
2
+
3
+ Usage::
4
+
5
+ from fastapi import FastAPI
6
+ from hostess_sdk.fastapi import instrument
7
+
8
+ app = FastAPI()
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
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ from typing import TYPE_CHECKING, Iterable, Optional
20
+
21
+ from ._otel import (
22
+ build_resource,
23
+ ensure_tracer_provider,
24
+ is_enabled,
25
+ resolve_endpoint,
26
+ )
27
+ from ._otel.marker import start_marker_heartbeat
28
+
29
+ if TYPE_CHECKING:
30
+ from fastapi import FastAPI
31
+
32
+ logger = logging.getLogger("hostess_sdk")
33
+
34
+ _LANGUAGE = "python"
35
+ _FRAMEWORK = "fastapi"
36
+
37
+ # Apps already instrumented in this process, for idempotency.
38
+ _instrumented_apps: "set[int]" = set()
39
+
40
+
41
+ def instrument(
42
+ app: "FastAPI",
43
+ *,
44
+ enabled: Optional[bool] = None,
45
+ service_name: Optional[str] = None,
46
+ exclude_paths: Optional[Iterable[str]] = None,
47
+ otlp_endpoint: Optional[str] = None,
48
+ ) -> "FastAPI":
49
+ """Instrument a FastAPI app for Hostess API Insights.
50
+
51
+ - Applies ``opentelemetry-instrumentation-fastapi`` so requests produce
52
+ server spans carrying the route template, method, and status.
53
+ - Exports spans over OTLP/HTTP to the Hostess collector (endpoint injected
54
+ by the platform; overridable via ``otlp_endpoint`` for local use).
55
+ - Starts the marker heartbeat.
56
+
57
+ Idempotent, fail-silent, and a no-op when disabled or when no collector
58
+ endpoint can be resolved. Returns the original ``app`` for fluent use.
59
+ """
60
+ if not is_enabled(enabled):
61
+ logger.debug("hostess: instrumentation disabled; skipping")
62
+ return app
63
+
64
+ if id(app) in _instrumented_apps:
65
+ logger.debug("hostess: app already instrumented; skipping")
66
+ return app
67
+
68
+ endpoint = resolve_endpoint(otlp_endpoint)
69
+ if endpoint is None:
70
+ # Common when running locally outside Hostess: stay a clean no-op.
71
+ logger.debug("hostess: no OTLP endpoint resolved; instrumentation is a no-op")
72
+ return app
73
+
74
+ sdk_version, framework_version = _versions()
75
+ resource = build_resource(
76
+ service_name=service_name,
77
+ sdk_version=sdk_version,
78
+ framework=_FRAMEWORK,
79
+ framework_version=framework_version,
80
+ )
81
+
82
+ try:
83
+ ensure_tracer_provider(resource, endpoint)
84
+ _instrument_fastapi(app, exclude_paths)
85
+ start_marker_heartbeat(
86
+ resource=resource,
87
+ base_endpoint=endpoint,
88
+ language=_LANGUAGE,
89
+ framework=_FRAMEWORK,
90
+ sdk_version=sdk_version,
91
+ framework_version=framework_version,
92
+ )
93
+ except Exception: # never break the host app over telemetry setup
94
+ logger.debug("hostess: instrumentation setup failed; continuing", exc_info=True)
95
+ return app
96
+
97
+ _instrumented_apps.add(id(app))
98
+ return app
99
+
100
+
101
+ def _instrument_fastapi(app: "FastAPI", exclude_paths: Optional[Iterable[str]]) -> None:
102
+ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
103
+
104
+ excluded_urls = ",".join(exclude_paths) if exclude_paths else None
105
+ FastAPIInstrumentor.instrument_app(app, excluded_urls=excluded_urls)
106
+
107
+
108
+ def _versions() -> "tuple[str, str]":
109
+ from importlib.metadata import PackageNotFoundError
110
+ from importlib.metadata import version as pkg_version
111
+
112
+ def safe(name: str) -> str:
113
+ try:
114
+ return pkg_version(name)
115
+ except PackageNotFoundError:
116
+ return "unknown"
117
+
118
+ return safe("hostess-python"), safe("fastapi")
119
+
120
+
121
+ def _reset_for_tests() -> None:
122
+ """Clear per-process idempotency state (test helper)."""
123
+ _instrumented_apps.clear()