Metadata-Version: 2.5
Name: craftainer-tool-observability
Version: 0.1.1
Summary: Health checks, structured logging, OpenTelemetry, request IDs and profiling for FastAPI services
License-Expression: GPL-3.0-or-later
License-File: LICENSE
Requires-Python: >=3.14
Requires-Dist: fastapi<1,>=0.141.1
Provides-Extra: http
Requires-Dist: httpx<1,>=0.28.1; extra == 'http'
Provides-Extra: otel
Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.44.0; extra == 'otel'
Requires-Dist: opentelemetry-instrumentation-fastapi<1,>=0.65b0; extra == 'otel'
Requires-Dist: opentelemetry-instrumentation-logging<1,>=0.65b0; extra == 'otel'
Requires-Dist: opentelemetry-instrumentation-sqlalchemy<1,>=0.65b0; extra == 'otel'
Requires-Dist: opentelemetry-sdk<2,>=1.44.0; extra == 'otel'
Requires-Dist: sqlalchemy[asyncio]<2.1,>=2.0.54; extra == 'otel'
Provides-Extra: profiling
Requires-Dist: pyinstrument<6,>=5.1.3; extra == 'profiling'
Provides-Extra: redis
Requires-Dist: redis<9,>=8.1.0; extra == 'redis'
Provides-Extra: s3
Requires-Dist: boto3<2,>=1.43.98; extra == 's3'
Provides-Extra: sqlalchemy
Requires-Dist: sqlalchemy[asyncio]<2.1,>=2.0.54; extra == 'sqlalchemy'
Description-Content-Type: text/markdown

# craftainer-tool-observability

> [!NOTE]
> This repository is vibe coded with [Claude](https://claude.com/product/claude-code).

Observability building blocks for FastAPI services: a health check framework
with `/live` and `/ready` routes and generic dependency checks, structured
JSON logging, OpenTelemetry traces and metrics, a request-ID middleware and
opt-in request profiling. It is extracted from
[`template-fastapi`](https://github.com/craftainer/template-fastapi) and is an
instance of
[`template-python-package`](https://github.com/craftainer/template-python-package)
(see [`docs/TEMPLATE.md`](docs/TEMPLATE.md) for the conventions shared with
every instance). It never imports an application package or
`craftainer_tool_crud`; an import-linter contract enforces that.

## Install

The distribution is `craftainer-tool-observability`, the import package
`craftainer_tool_observability`. The core needs FastAPI only; everything else
is an extra, so install what you use:

```sh
pip install craftainer-tool-observability                    # health framework, request IDs
pip install "craftainer-tool-observability[otel,redis]"      # plus tracing/metrics/logs, Redis check
```

| Extra | Brings | Used by |
| --- | --- | --- |
| `otel` | OpenTelemetry SDK, OTLP/HTTP exporters, logging, FastAPI and SQLAlchemy instrumentations (and SQLAlchemy, which `otel.py` imports for typing) | `logs.py`, `otel.py` |
| `sqlalchemy` | SQLAlchemy (asyncio) | `health.checks.postgres.PostgresCheck` |
| `redis` | redis-py | `health.checks.redis.RedisCheck` |
| `s3` | boto3 | `health.checks.s3.S3Check` |
| `http` | httpx | `health.checks.http.HTTPCheck` |
| `profiling` | pyinstrument | `profiling.install_profiling` |

`logs.py` and `otel.py` import OpenTelemetry at module level, so import them
only when `[otel]` is installed. The package root, `health` and `middleware`
import with no extra installed (a unit test proves it). Note that
`opentelemetry-sqlalchemy` instrumentation supports SQLAlchemy below 2.1 only,
hence the `<2.1` bound.

## Public API

| Module | Provides |
| --- | --- |
| `health` | `HealthCheck`, `HealthCheckResult`, `HealthRegistry`, `build_health_router(get_registry, include_info=False, info_package=None)` |
| `health.checks` | `PostgresCheck`, `RedisCheck`, `S3Check`, `HTTPCheck` (one module each) |
| `logs` | `configure_logging()`, `shared_resource()` |
| `otel` | `configure_telemetry()`, `instrument_app(app)`, `instrument_engine(engine)` |
| `middleware` | `RequestIDMiddleware`, `get_request_id()` |
| `profiling` | `install_profiling(app, *, enabled, production, query_param="profile")` |

The details, environment variables and conventions are in
[`src/craftainer_tool_observability/README.md`](src/craftainer_tool_observability/README.md).

## Versioning

SemVer, starting at `0.1.0`. While the major version is 0, a minor release may
change the public API; pin `craftainer-tool-observability>=0.x,<0.(x+1)` until
1.0. Real-service integration tests for the dependency checks stay in
`template-fastapi`; the unit tests here use fakes and need no backing service.

## Development

```sh
uv sync
prek run --all-files --hook-stage manual   # lint, types, import contracts, unit tests (100% coverage)
uv run pytest tests/bench --no-cov         # benchmarks (CodSpeed in CI)
```

## Publishing to PyPI

Releases use PyPI [trusted publishing](https://docs.pypi.org/trusted-publishers/)
(GitHub OIDC): no API token is stored anywhere. `make publish` is a no-op
until you opt in, per repository:

1. Register a "pending publisher" on the index before the first release
   (PyPI: Your account, Publishing, "Add a new pending publisher"; TestPyPI
   has its own, separate form on test.pypi.org). Enter the project name from
   `pyproject.toml`, the GitHub owner and repository, the workflow name
   `release.yml`, and leave the environment empty (or use the one
   `release.yml` runs under, if you add one).
2. Set the repository variable `PYPI_PUBLISH` (Settings, Secrets and
   variables, Actions, Variables):
   - unset: `make publish` prints a skip message and the release only
     attaches the sdist, wheel and SBOM to the GitHub release;
   - `testpypi`: publish to TestPyPI;
   - `pypi`: publish to PyPI.
3. Do the first release with `testpypi` as a dry run (a pre-release channel
   is enough), confirm that `make release-check` installs the published
   version back from TestPyPI, then register the PyPI pending publisher and
   switch the variable to `pypi`.

The version lives only in the git tag `release.yml` computes; `pyproject.toml`
stays at `0.0.0` and `make build` stamps `RELEASE_VERSION` into the build
without committing it.

## License

Licensed under the [GNU General Public License v3.0](LICENSE).
