Metadata-Version: 2.5
Name: papeete-observability
Version: 0.1.0
Summary: Cross-cutting OpenTelemetry wiring for the papeete-* ecosystem — one configure() call, OTLP/gRPC export to Tempo/Loki/Prometheus.
Author-email: Papeete Consulting <yoann.remy@outlook.com>
License-Expression: MIT
Keywords: logging,metrics,observability,opentelemetry,tracing
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.11
Classifier: Topic :: System :: Logging
Classifier: Topic :: System :: Monitoring
Requires-Python: >=3.11
Requires-Dist: opentelemetry-api>=1.27
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.27
Requires-Dist: opentelemetry-sdk>=1.27
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# papeete-observability

Cross-cutting OpenTelemetry wiring for the papeete-* ecosystem — one function, `configure()`,
that turns the OpenTelemetry API's own no-ops (already used by
[`papeete-actor-synchronous-messaging-http`](https://github.com/papeete-hub/papeete-actor-synchronous-messaging-http)'s
`_tracing.py`) into a real `TracerProvider`/`MeterProvider`/`LoggerProvider`, exporting over OTLP
gRPC.

```bash
pip install papeete-observability
```

## Why a separate package

Observability is uniform across every papeete-* binding and every door — unlike
`papeete-actor`'s `Engine` port, where *which vendor* genuinely varies per call site,
there is exactly one shape of "start a span, record a metric, extract a context" this whole
ecosystem needs. OpenTelemetry's own API already is that port: `trace.get_tracer(...)` and
`propagate.inject()`/`extract()` are genuine no-ops with no SDK configured. This package's only
job is configuring that SDK once, at process startup — see
[ADR-PO-0001](./adr/ADR-PO-0001-a-standalone-repo-for-cross-cutting-telemetry.md) for the full
rationale, including why this is a standalone repo rather than a module inside a binding.

## `configure()`

```python
from papeete_observability import configure

configure()          # every argument optional — falls back to OTel's own standard env vars
```

Call it once, before constructing a `Mailbox`:

```python
from papeete_observability import configure
from papeete_actor_synchronous_messaging_http.mailbox import HttpMailbox

configure(service_name="waiter", otlp_endpoint="otel-collector.observability.svc.cluster.local:4317")

box = HttpMailbox()
```

| Argument | Falls back to | Notes |
|---|---|---|
| `service_name` | `OTEL_SERVICE_NAME` | identifies this process in Tempo/Loki/Prometheus |
| `otlp_endpoint` | `OTEL_EXPORTER_OTLP_ENDPOINT` | gRPC, e.g. `otel-collector.observability.svc.cluster.local:4317` |
| `resource_attributes` | `OTEL_RESOURCE_ATTRIBUTES` | extra `Resource` attributes, e.g. `{"deployment.environment": "local"}` |

A deployment that only sets the k8s env vars above needs no Python-side argument at all —
`configure()` with no arguments picks them all up.

## What `configure()` wires

- **Traces** (`tracing.py`) — `TracerProvider` + `BatchSpanProcessor` + `OTLPSpanExporter`
  (gRPC). Every `start_as_current_span(...)` call anywhere in the process starts actually
  recording and exporting, full parent/child propagation via W3C `traceparent` headers.
- **Metrics** (`metrics.py`) — `MeterProvider` + `PeriodicExportingMetricReader` +
  `OTLPMetricExporter` (gRPC). Pushed over the same Collector endpoint traces use — no `/metrics`
  scrape route anywhere in this ecosystem.
- **Logs** (`logging_.py`) — `LoggerProvider` + `BatchLogRecordProcessor` + `OTLPLogExporter`
  (gRPC), with a `LoggingHandler` attached to the root stdlib `logging` logger. Any plain
  `logging.info(...)` call anywhere in the process (e.g. `HttpMailbox`'s own instrumentation
  points) becomes an OTLP-correlated log record automatically — no code change needed beyond
  `configure()` having run once.

## The backend

This package only configures *where* telemetry goes; it doesn't stand up what receives it.
[`papeete-platform`](https://github.com/papeete-hub/papeete-platform)'s
`modules/observability/` Terraform module provisions the other half: an OTel Collector
(gRPC receiver on 4317, fanning out to exporters), Tempo for traces, Loki for logs, Prometheus
for metrics, and a Grafana with all three pre-provisioned as datasources. Kibana/Elasticsearch
are not part of that default stack — see that module's own README.

## Tests

```bash
uv run --extra dev pytest
```

No live OTLP Collector needed: `BatchSpanProcessor`/`PeriodicExportingMetricReader`/
`BatchLogRecordProcessor` all export on their own background schedule, never synchronously at
`configure()` time, so the suite proves the SDK is wired up without a real backend.

## Releasing

Tag-triggered, via [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC).
**No API token is stored anywhere** — GitHub mints a short-lived OIDC token per run and PyPI
trades it for an upload token. There is nothing to rotate and nothing to leak.

```bash
git tag v0.1.0 && git push origin v0.1.0     # .github/workflows/release.yml does the rest
```

### One-time setup

**1. A pending publisher on PyPI** — the project does not exist yet, so it is registered from
the publisher side rather than by a first manual upload. At
<https://pypi.org/manage/account/publishing/>, as a **GitHub** pending publisher:

| Field | Value |
|---|---|
| PyPI Project Name | `papeete-observability` |
| Owner | `papeete-hub` |
| Repository name | `papeete-observability` |
| Workflow name | `release.yml` |
| Environment name | `pypi` |

All five must match exactly — PyPI checks the OIDC claims against them and rejects the upload
otherwise. `release.yml` already declares `permissions: id-token: write` and
`environment: pypi`, which is what makes those claims present.

**2. A `pypi` GitHub environment**, with no secrets in it — it exists so the OIDC claim carries
an environment name for PyPI to match. Protection rules are **not** set by default and are worth
considering, because a release is irreversible: PyPI never allows re-uploading a version, even
after a delete. Required reviewers, and restricting deployments to tags matching `v*`, are the
two that earn their keep.

After the first successful release PyPI converts the pending publisher into a normal one
automatically; there is no second setup step.

### What a release asserts

The workflow builds, installs the wheel into a clean venv, and imports `configure` from it —
calling it once, with no OTLP Collector present, to prove it does not raise — before publishing,
so a build that can't actually be used fails the release instead of shipping a package nobody
can use. It then re-installs the exact version just published, from PyPI itself, polling for CDN
propagation rather than trusting the upload step's own exit code.

## Licence

MIT.
