carros-ai-utils 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.
- carros_ai_utils-0.2.0/.github/workflows/pr-agent.yml +22 -0
- carros_ai_utils-0.2.0/.github/workflows/publish.yml +24 -0
- carros_ai_utils-0.2.0/.gitignore +12 -0
- carros_ai_utils-0.2.0/PKG-INFO +32 -0
- carros_ai_utils-0.2.0/README.md +109 -0
- carros_ai_utils-0.2.0/openspec/changes/archive/2026-04-03-init-telemetry-python/design.md +65 -0
- carros_ai_utils-0.2.0/openspec/changes/archive/2026-04-03-init-telemetry-python/proposal.md +24 -0
- carros_ai_utils-0.2.0/openspec/changes/archive/2026-04-03-init-telemetry-python/specs/config/spec.md +55 -0
- carros_ai_utils-0.2.0/openspec/changes/archive/2026-04-03-init-telemetry-python/specs/fastapi-middleware/spec.md +58 -0
- carros_ai_utils-0.2.0/openspec/changes/archive/2026-04-03-init-telemetry-python/specs/init/spec.md +51 -0
- carros_ai_utils-0.2.0/openspec/changes/archive/2026-04-03-init-telemetry-python/specs/logger/spec.md +50 -0
- carros_ai_utils-0.2.0/openspec/changes/archive/2026-04-03-init-telemetry-python/specs/meter/spec.md +38 -0
- carros_ai_utils-0.2.0/openspec/changes/archive/2026-04-03-init-telemetry-python/specs/otlp-exporter/spec.md +45 -0
- carros_ai_utils-0.2.0/openspec/changes/archive/2026-04-03-init-telemetry-python/specs/tracer/spec.md +34 -0
- carros_ai_utils-0.2.0/openspec/changes/archive/2026-04-03-init-telemetry-python/tasks.md +49 -0
- carros_ai_utils-0.2.0/openspec/config.yaml +74 -0
- carros_ai_utils-0.2.0/openspec/specs/config/spec.md +55 -0
- carros_ai_utils-0.2.0/openspec/specs/fastapi-middleware/spec.md +58 -0
- carros_ai_utils-0.2.0/openspec/specs/init/spec.md +51 -0
- carros_ai_utils-0.2.0/openspec/specs/logger/spec.md +50 -0
- carros_ai_utils-0.2.0/openspec/specs/meter/spec.md +38 -0
- carros_ai_utils-0.2.0/openspec/specs/otlp-exporter/spec.md +45 -0
- carros_ai_utils-0.2.0/openspec/specs/tracer/spec.md +34 -0
- carros_ai_utils-0.2.0/pyproject.toml +56 -0
- carros_ai_utils-0.2.0/renovate.json +26 -0
- carros_ai_utils-0.2.0/ruff.toml +12 -0
- carros_ai_utils-0.2.0/src/carros_ai_utils/__init__.py +134 -0
- carros_ai_utils-0.2.0/src/carros_ai_utils/_config.py +41 -0
- carros_ai_utils-0.2.0/src/carros_ai_utils/_context.py +79 -0
- carros_ai_utils-0.2.0/src/carros_ai_utils/_exporters.py +34 -0
- carros_ai_utils-0.2.0/src/carros_ai_utils/_prometheus.py +30 -0
- carros_ai_utils-0.2.0/src/carros_ai_utils/_providers.py +51 -0
- carros_ai_utils-0.2.0/src/carros_ai_utils/_settings.py +64 -0
- carros_ai_utils-0.2.0/src/carros_ai_utils/_state.py +10 -0
- carros_ai_utils-0.2.0/src/carros_ai_utils/_structlog.py +103 -0
- carros_ai_utils-0.2.0/src/carros_ai_utils/fastapi.py +48 -0
- carros_ai_utils-0.2.0/src/carros_ai_utils/settings.py +21 -0
- carros_ai_utils-0.2.0/tests/conftest.py +23 -0
- carros_ai_utils-0.2.0/tests/test_config.py +60 -0
- carros_ai_utils-0.2.0/tests/test_context.py +47 -0
- carros_ai_utils-0.2.0/tests/test_exporters.py +32 -0
- carros_ai_utils-0.2.0/tests/test_fastapi.py +78 -0
- carros_ai_utils-0.2.0/tests/test_init.py +72 -0
- carros_ai_utils-0.2.0/tests/test_providers.py +58 -0
- carros_ai_utils-0.2.0/tests/test_settings.py +81 -0
- carros_ai_utils-0.2.0/tests/test_structlog.py +56 -0
- carros_ai_utils-0.2.0/uv.lock +1283 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: PR Agent
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, reopened, ready_for_review, synchronize]
|
|
6
|
+
issue_comment:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
issues: write
|
|
10
|
+
pull-requests: write
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
review:
|
|
15
|
+
uses: carros-ai/platform-ci-workflows/.github/workflows/pr-agent.yml@main
|
|
16
|
+
permissions:
|
|
17
|
+
issues: write
|
|
18
|
+
pull-requests: write
|
|
19
|
+
contents: read
|
|
20
|
+
secrets:
|
|
21
|
+
ANTHROPIC_KEY: ${{ secrets.ANTHROPIC_KEY }}
|
|
22
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ['v*']
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: '3.13'
|
|
19
|
+
|
|
20
|
+
- name: Build
|
|
21
|
+
run: pip install build && python -m build
|
|
22
|
+
|
|
23
|
+
- name: Publish
|
|
24
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: carros-ai-utils
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Opinionated OpenTelemetry wrapper for carros-ai Python services
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.25.0
|
|
7
|
+
Requires-Dist: opentelemetry-instrumentation-logging>=0.46b0
|
|
8
|
+
Requires-Dist: opentelemetry-sdk>=1.25.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: bandit>=1.7; extra == 'dev'
|
|
11
|
+
Requires-Dist: fastapi>=0.111.0; extra == 'dev'
|
|
12
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
13
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
14
|
+
Requires-Dist: opentelemetry-exporter-prometheus>=0.46b0; extra == 'dev'
|
|
15
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.46b0; extra == 'dev'
|
|
16
|
+
Requires-Dist: prometheus-client>=0.20; extra == 'dev'
|
|
17
|
+
Requires-Dist: pydantic-settings>=2.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
22
|
+
Requires-Dist: structlog>=23.0; extra == 'dev'
|
|
23
|
+
Provides-Extra: fastapi
|
|
24
|
+
Requires-Dist: fastapi>=0.111.0; extra == 'fastapi'
|
|
25
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.46b0; extra == 'fastapi'
|
|
26
|
+
Provides-Extra: prometheus
|
|
27
|
+
Requires-Dist: opentelemetry-exporter-prometheus>=0.46b0; extra == 'prometheus'
|
|
28
|
+
Requires-Dist: prometheus-client>=0.20; extra == 'prometheus'
|
|
29
|
+
Provides-Extra: settings
|
|
30
|
+
Requires-Dist: pydantic-settings>=2.0; extra == 'settings'
|
|
31
|
+
Provides-Extra: structlog
|
|
32
|
+
Requires-Dist: structlog>=23.0; extra == 'structlog'
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# carros-ai-utils
|
|
2
|
+
|
|
3
|
+
Opinionated OpenTelemetry wrapper for carros-ai Python services. Single call wires traces, metrics, and logs — all exported via OTLP gRPC to the observability collector.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```toml
|
|
8
|
+
# pyproject.toml
|
|
9
|
+
[project]
|
|
10
|
+
dependencies = [
|
|
11
|
+
"carros-ai-utils @ git+ssh://git@github.com/carros-ai/python-utils.git",
|
|
12
|
+
]
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
With FastAPI support:
|
|
16
|
+
|
|
17
|
+
```toml
|
|
18
|
+
dependencies = [
|
|
19
|
+
"carros-ai-utils[fastapi] @ git+ssh://git@github.com/carros-ai/python-utils.git",
|
|
20
|
+
]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### FastAPI
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
# main.py
|
|
29
|
+
from contextlib import asynccontextmanager
|
|
30
|
+
|
|
31
|
+
import carros_ai_utils
|
|
32
|
+
from carros_ai_utils.fastapi import instrument_fastapi
|
|
33
|
+
from fastapi import FastAPI
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@asynccontextmanager
|
|
37
|
+
async def lifespan(app: FastAPI):
|
|
38
|
+
carros_ai_utils.init("payments-api")
|
|
39
|
+
instrument_fastapi(app, "payments-api")
|
|
40
|
+
yield
|
|
41
|
+
carros_ai_utils.shutdown()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
app = FastAPI(lifespan=lifespan)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Plain Python / scripts
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import carros_ai_utils
|
|
51
|
+
|
|
52
|
+
carros_ai_utils.init("my-worker")
|
|
53
|
+
|
|
54
|
+
# traces
|
|
55
|
+
with carros_ai_utils.tracer("my-module").start_as_current_span("process-job") as span:
|
|
56
|
+
span.set_attribute("job.id", job_id)
|
|
57
|
+
do_work()
|
|
58
|
+
|
|
59
|
+
# metrics
|
|
60
|
+
counter = carros_ai_utils.meter("my-module").create_counter("jobs.processed")
|
|
61
|
+
counter.add(1, {"queue": "default"})
|
|
62
|
+
|
|
63
|
+
carros_ai_utils.shutdown()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Logging
|
|
67
|
+
|
|
68
|
+
After `carros_ai_utils.init()`, Python's standard `logging` module is automatically bridged to OTLP. No extra setup needed:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import logging
|
|
72
|
+
|
|
73
|
+
logger = logging.getLogger(__name__)
|
|
74
|
+
logger.info("job started", extra={"job_id": "abc123"})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Environment variables
|
|
78
|
+
|
|
79
|
+
| Variable | Default | Description |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `localhost:4317` | Collector gRPC endpoint |
|
|
82
|
+
| `APP_ENVIRONMENT` | `development` | Set to `production` to activate OTLP export |
|
|
83
|
+
| `APP_VERSION` | `0.0.0` | Reported as `service.version` resource attribute |
|
|
84
|
+
| `OTEL_SERVICE_NAME` | _(init arg)_ | Overrides the `service_name` passed to `carros_ai_utils.init()` |
|
|
85
|
+
|
|
86
|
+
In **development** (default), all signals are printed to the console. In **production**, they are exported via gRPC to `OTEL_EXPORTER_OTLP_ENDPOINT`.
|
|
87
|
+
|
|
88
|
+
## Kubernetes / Coolify deployment
|
|
89
|
+
|
|
90
|
+
Add these env vars to your `deployment.yaml` or Coolify service:
|
|
91
|
+
|
|
92
|
+
```yaml
|
|
93
|
+
- name: APP_ENVIRONMENT
|
|
94
|
+
value: "production"
|
|
95
|
+
- name: APP_VERSION
|
|
96
|
+
value: "1.0.0"
|
|
97
|
+
- name: OTEL_EXPORTER_OTLP_ENDPOINT
|
|
98
|
+
value: "metrics-collector-api.application.svc.cluster.local:4317"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The `OTEL_SERVICE_NAME` is optional — if omitted, the name passed to `carros_ai_utils.init()` is used.
|
|
102
|
+
|
|
103
|
+
## FastAPI middleware behavior
|
|
104
|
+
|
|
105
|
+
`instrument_fastapi` automatically:
|
|
106
|
+
|
|
107
|
+
- Creates a span for every HTTP request
|
|
108
|
+
- Propagates trace context from incoming headers (`traceparent`)
|
|
109
|
+
- Copies the `X-User-Id` header (injected by Traefik forward-auth) into the span as `enduser.id`
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
## Context
|
|
2
|
+
The OpenTelemetry Python SDK requires configuring three independent providers
|
|
3
|
+
(TracerProvider, MeterProvider, LoggerProvider), each with its own exporter,
|
|
4
|
+
resource, and global registration. This library does all of that in one call
|
|
5
|
+
and exposes a minimal, stable surface for application code.
|
|
6
|
+
|
|
7
|
+
## Goals
|
|
8
|
+
1. Single init() call configures all three signal types
|
|
9
|
+
2. No global state leakage between tests — providers can be reset
|
|
10
|
+
3. Environment-driven config — no config files, no hardcoded values
|
|
11
|
+
4. Works in production (gRPC to collector) and locally (stdout, no collector needed)
|
|
12
|
+
5. FastAPI integration requires zero manual span management per route
|
|
13
|
+
|
|
14
|
+
## Technical Decisions
|
|
15
|
+
|
|
16
|
+
### Decision: OTLP gRPC only (no HTTP exporter)
|
|
17
|
+
ALTERNATIVES: Support both gRPC and HTTP
|
|
18
|
+
RISKS: Environments that block HTTP/2 cannot use this library
|
|
19
|
+
RATIONALE: Matches the telemetry-collector which only exposes gRPC. All
|
|
20
|
+
carros-ai services run inside private Docker networks where gRPC is fully
|
|
21
|
+
supported. Adding HTTP adds a second code path with no current consumer.
|
|
22
|
+
|
|
23
|
+
### Decision: APP_ENVIRONMENT drives exporter selection
|
|
24
|
+
ALTERNATIVES: Separate env var (e.g. OTEL_PYTHON_EXPORTER=grpc|stdout)
|
|
25
|
+
RISKS: APP_ENVIRONMENT may be used for other purposes in the app
|
|
26
|
+
RATIONALE: Consistent with the Go telemetry package convention already in use.
|
|
27
|
+
When APP_ENVIRONMENT=production, gRPC to the collector is used. Any other
|
|
28
|
+
value falls back to stdout (ConsoleExporter), allowing local development
|
|
29
|
+
without running the collector.
|
|
30
|
+
|
|
31
|
+
### Decision: Python logging bridge via LoggingHandler
|
|
32
|
+
ALTERNATIVES: Provide a custom logger API; use structlog
|
|
33
|
+
RISKS: LoggingHandler adds latency to every log call via OTLP export
|
|
34
|
+
RATIONALE: Application code already uses logging.getLogger(). Bridging it
|
|
35
|
+
to OTLP requires zero changes in application code — only telemetry.init()
|
|
36
|
+
needs to be called at startup. Custom logger APIs would require refactoring
|
|
37
|
+
every existing log call.
|
|
38
|
+
|
|
39
|
+
### Decision: FastAPI middleware via OpenTelemetry FastAPI instrumentation
|
|
40
|
+
ALTERNATIVES: Manual span creation in route handlers; custom ASGI middleware
|
|
41
|
+
RISKS: Upstream instrumentation library may lag behind FastAPI releases
|
|
42
|
+
RATIONALE: opentelemetry-instrumentation-fastapi is the official, maintained
|
|
43
|
+
instrumentation. It handles span creation, HTTP attribute population, and
|
|
44
|
+
error capture automatically. Manual spans would require changes in every route.
|
|
45
|
+
|
|
46
|
+
### Decision: Frozen dataclass for Config (not Pydantic)
|
|
47
|
+
ALTERNATIVES: Pydantic BaseSettings; plain dict; os.getenv() calls scattered
|
|
48
|
+
RISKS: Less validation than Pydantic; no .env file loading
|
|
49
|
+
RATIONALE: This is a library, not an application. Adding Pydantic as a
|
|
50
|
+
required dependency would force it on every consumer. A frozen dataclass
|
|
51
|
+
with explicit type coercion keeps the dependency footprint minimal.
|
|
52
|
+
If a consumer already uses Pydantic, they can pass config values directly.
|
|
53
|
+
|
|
54
|
+
## Migration
|
|
55
|
+
Existing services add one dependency to pyproject.toml and one call in main:
|
|
56
|
+
```python
|
|
57
|
+
import telemetry
|
|
58
|
+
telemetry.init("my-service")
|
|
59
|
+
```
|
|
60
|
+
No other changes required. Existing logging.getLogger() calls are
|
|
61
|
+
automatically bridged to OTLP after init().
|
|
62
|
+
|
|
63
|
+
## Open Questions
|
|
64
|
+
- Should meter() expose shorthand helpers (counter, histogram) or only the raw Meter?
|
|
65
|
+
- Should init() accept a shutdown_timeout parameter for graceful flush?
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
## Why
|
|
2
|
+
Every Python service in the carros-ai platform must configure OpenTelemetry
|
|
3
|
+
providers, exporters, and resource attributes individually. This is repetitive,
|
|
4
|
+
error-prone, and leads to inconsistent observability across services. A shared
|
|
5
|
+
library encapsulates this once — services call telemetry.init("name") and get
|
|
6
|
+
fully configured traces, metrics, and logs with zero boilerplate.
|
|
7
|
+
|
|
8
|
+
## What Changes
|
|
9
|
+
Initial creation of the telemetry-python library. No existing services are
|
|
10
|
+
modified. All capabilities are ADDED.
|
|
11
|
+
|
|
12
|
+
## Capabilities
|
|
13
|
+
- **config** — Reads observability settings from environment variables with sensible defaults
|
|
14
|
+
- **otlp-exporter** — Configures OTLP gRPC exporters pointing to the telemetry-collector
|
|
15
|
+
- **init** — Single entry point that wires all providers with one function call
|
|
16
|
+
- **tracer** — Provides a pre-configured Tracer for creating spans
|
|
17
|
+
- **meter** — Provides a pre-configured Meter for recording metrics
|
|
18
|
+
- **logger** — Bridges Python standard logging to OTLP via LoggingHandler
|
|
19
|
+
- **fastapi-middleware** — Auto-instruments FastAPI apps (spans per request, error capture)
|
|
20
|
+
|
|
21
|
+
## Impact
|
|
22
|
+
- New standalone library — no existing services are modified
|
|
23
|
+
- Services add one dependency and one init() call at startup
|
|
24
|
+
- Requires the telemetry-collector to be reachable at OTEL_EXPORTER_OTLP_ENDPOINT
|
carros_ai_utils-0.2.0/openspec/changes/archive/2026-04-03-init-telemetry-python/specs/config/spec.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
## Purpose
|
|
2
|
+
Read observability settings from environment variables and expose them as an
|
|
3
|
+
immutable Config object. Provides sensible defaults so services work locally
|
|
4
|
+
without any environment setup.
|
|
5
|
+
|
|
6
|
+
### Requirement: Config from Environment Variables
|
|
7
|
+
The library SHALL read configuration exclusively from environment variables,
|
|
8
|
+
applying defaults for any variable that is not set.
|
|
9
|
+
|
|
10
|
+
#### Scenario: All defaults applied when no env vars set
|
|
11
|
+
- **GIVEN** no observability-related environment variables are set
|
|
12
|
+
- **WHEN** Config is instantiated
|
|
13
|
+
- **THEN** `endpoint` is `"localhost:4317"`
|
|
14
|
+
- **AND** `environment` is `"development"`
|
|
15
|
+
- **AND** `version` is `"0.0.0"`
|
|
16
|
+
- **AND** `service_name` is `None` (overridden by init() argument)
|
|
17
|
+
|
|
18
|
+
#### Scenario: Production values read from environment
|
|
19
|
+
- **GIVEN** the following env vars are set:
|
|
20
|
+
```
|
|
21
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=telemetry-collector:4317
|
|
22
|
+
APP_ENVIRONMENT=production
|
|
23
|
+
APP_VERSION=1.2.3
|
|
24
|
+
OTEL_SERVICE_NAME=payments-api
|
|
25
|
+
```
|
|
26
|
+
- **WHEN** Config is instantiated
|
|
27
|
+
- **THEN** each field reflects the corresponding env var value
|
|
28
|
+
|
|
29
|
+
#### Scenario: OTEL_SERVICE_NAME overrides init() argument
|
|
30
|
+
- **GIVEN** `OTEL_SERVICE_NAME=env-name` is set
|
|
31
|
+
- **WHEN** `telemetry.init("code-name")` is called
|
|
32
|
+
- **THEN** the effective service name used in the resource is `"env-name"`
|
|
33
|
+
|
|
34
|
+
### Requirement: Immutable Config
|
|
35
|
+
The Config object SHALL be immutable after instantiation — no field may be
|
|
36
|
+
modified by application code.
|
|
37
|
+
|
|
38
|
+
#### Scenario: Mutation attempt raises error
|
|
39
|
+
- **GIVEN** a Config instance
|
|
40
|
+
- **WHEN** application code attempts to set any field (e.g. `cfg.endpoint = "x"`)
|
|
41
|
+
- **THEN** a `FrozenInstanceError` or `AttributeError` is raised
|
|
42
|
+
|
|
43
|
+
### Requirement: Production Mode Detection
|
|
44
|
+
The library SHALL treat `APP_ENVIRONMENT=production` as the signal to use
|
|
45
|
+
gRPC OTLP export. Any other value (including unset) uses stdout export.
|
|
46
|
+
|
|
47
|
+
#### Scenario: Production mode active
|
|
48
|
+
- **GIVEN** `APP_ENVIRONMENT=production`
|
|
49
|
+
- **WHEN** `config.is_production` is accessed
|
|
50
|
+
- **THEN** it returns `True`
|
|
51
|
+
|
|
52
|
+
#### Scenario: Development mode when env var absent
|
|
53
|
+
- **GIVEN** `APP_ENVIRONMENT` is not set
|
|
54
|
+
- **WHEN** `config.is_production` is accessed
|
|
55
|
+
- **THEN** it returns `False`
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
## Purpose
|
|
2
|
+
Provide zero-config FastAPI auto-instrumentation that creates a span per
|
|
3
|
+
HTTP request, captures standard HTTP attributes, and records errors —
|
|
4
|
+
without requiring manual span management in route handlers.
|
|
5
|
+
|
|
6
|
+
### Requirement: Automatic Span per Request
|
|
7
|
+
After instrumentation is applied, the library SHALL create an OTel span for
|
|
8
|
+
every incoming HTTP request handled by FastAPI.
|
|
9
|
+
|
|
10
|
+
#### Scenario: Span created for successful request
|
|
11
|
+
- **GIVEN** `telemetry.init("api")` was called and instrumentation applied
|
|
12
|
+
- **WHEN** a GET request to `/items/42` is handled and returns 200
|
|
13
|
+
- **THEN** a span named `"GET /items/{item_id}"` is created
|
|
14
|
+
- **AND** it has attributes: `http.method=GET`, `http.status_code=200`,
|
|
15
|
+
`http.route=/items/{item_id}`
|
|
16
|
+
- **AND** the span status is OK
|
|
17
|
+
|
|
18
|
+
#### Scenario: Span records 5xx errors
|
|
19
|
+
- **GIVEN** instrumentation is applied
|
|
20
|
+
- **WHEN** a route raises an unhandled exception (500)
|
|
21
|
+
- **THEN** the span status is set to ERROR
|
|
22
|
+
- **AND** the exception is recorded on the span via `span.record_exception()`
|
|
23
|
+
|
|
24
|
+
#### Scenario: Span records 4xx without error status
|
|
25
|
+
- **GIVEN** instrumentation is applied
|
|
26
|
+
- **WHEN** a route returns 404
|
|
27
|
+
- **THEN** the span is created with `http.status_code=404`
|
|
28
|
+
- **AND** the span status is NOT set to ERROR (4xx are not server errors)
|
|
29
|
+
|
|
30
|
+
### Requirement: Lifespan Integration
|
|
31
|
+
The library SHALL provide a FastAPI lifespan context manager that calls
|
|
32
|
+
`telemetry.init()` on startup and `telemetry.shutdown()` on shutdown.
|
|
33
|
+
|
|
34
|
+
#### Scenario: Init called on app startup
|
|
35
|
+
- **GIVEN** a FastAPI app uses `telemetry.fastapi.lifespan("svc")`
|
|
36
|
+
- **WHEN** the app starts
|
|
37
|
+
- **THEN** `telemetry.init("svc")` is called before the first request
|
|
38
|
+
|
|
39
|
+
#### Scenario: Shutdown called on app shutdown
|
|
40
|
+
- **GIVEN** a FastAPI app uses `telemetry.fastapi.lifespan("svc")`
|
|
41
|
+
- **WHEN** the app receives SIGTERM and begins shutdown
|
|
42
|
+
- **THEN** `telemetry.shutdown()` is called, flushing pending signals
|
|
43
|
+
|
|
44
|
+
### Requirement: Opt-in Instrumentation
|
|
45
|
+
Instrumentation SHALL NOT be applied automatically on import — the application
|
|
46
|
+
must explicitly call the instrumentation function or use the lifespan helper.
|
|
47
|
+
|
|
48
|
+
#### Scenario: Import does not instrument
|
|
49
|
+
- **GIVEN** `import telemetry.fastapi` was executed
|
|
50
|
+
- **WHEN** no instrumentation call is made
|
|
51
|
+
- **THEN** no spans are created for requests
|
|
52
|
+
- **AND** no global state is modified
|
|
53
|
+
|
|
54
|
+
#### Scenario: Explicit instrumentation applies
|
|
55
|
+
- **GIVEN** an existing FastAPI app instance `app`
|
|
56
|
+
- **WHEN** `telemetry.fastapi.instrument(app)` is called
|
|
57
|
+
- **THEN** the FastAPIInstrumentor is applied to `app`
|
|
58
|
+
- **AND** all subsequent requests generate spans
|
carros_ai_utils-0.2.0/openspec/changes/archive/2026-04-03-init-telemetry-python/specs/init/spec.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
## Purpose
|
|
2
|
+
Provide a single entry point that configures all OpenTelemetry providers
|
|
3
|
+
(traces, metrics, logs) and registers them globally, so application code
|
|
4
|
+
needs only one call at startup.
|
|
5
|
+
|
|
6
|
+
### Requirement: Single Init Call
|
|
7
|
+
The library SHALL expose `telemetry.init(service_name)` as the sole required
|
|
8
|
+
setup call. After this call, traces, metrics, and logs are fully configured.
|
|
9
|
+
|
|
10
|
+
#### Scenario: Init configures all three providers
|
|
11
|
+
- **GIVEN** no providers have been configured
|
|
12
|
+
- **WHEN** `telemetry.init("payments-api")` is called
|
|
13
|
+
- **THEN** a TracerProvider is set as the global OTel tracer provider
|
|
14
|
+
- **AND** a MeterProvider is set as the global OTel meter provider
|
|
15
|
+
- **AND** a LoggerProvider is set as the global OTel logger provider
|
|
16
|
+
- **AND** the Python root logger has a LoggingHandler attached
|
|
17
|
+
|
|
18
|
+
#### Scenario: Resource attributes set on all providers
|
|
19
|
+
- **GIVEN** `APP_VERSION=1.2.3` and `APP_ENVIRONMENT=production`
|
|
20
|
+
- **WHEN** `telemetry.init("payments-api")` is called
|
|
21
|
+
- **THEN** all providers share a Resource with:
|
|
22
|
+
- `service.name = "payments-api"`
|
|
23
|
+
- `service.version = "1.2.3"`
|
|
24
|
+
- `deployment.environment = "production"`
|
|
25
|
+
|
|
26
|
+
#### Scenario: OTEL_SERVICE_NAME overrides argument
|
|
27
|
+
- **GIVEN** `OTEL_SERVICE_NAME=env-override` is set
|
|
28
|
+
- **WHEN** `telemetry.init("code-name")` is called
|
|
29
|
+
- **THEN** `service.name` in the Resource is `"env-override"`
|
|
30
|
+
|
|
31
|
+
#### Scenario: Init is idempotent
|
|
32
|
+
- **GIVEN** `telemetry.init("svc")` was already called
|
|
33
|
+
- **WHEN** `telemetry.init("svc")` is called again
|
|
34
|
+
- **THEN** providers are not re-created or re-registered
|
|
35
|
+
- **AND** no error is raised
|
|
36
|
+
|
|
37
|
+
### Requirement: Graceful Shutdown
|
|
38
|
+
The library SHALL expose `telemetry.shutdown()` that flushes all pending
|
|
39
|
+
signals before the process exits.
|
|
40
|
+
|
|
41
|
+
#### Scenario: Shutdown flushes pending signals
|
|
42
|
+
- **GIVEN** `telemetry.init("svc")` was called and signals are buffered
|
|
43
|
+
- **WHEN** `telemetry.shutdown()` is called
|
|
44
|
+
- **THEN** all three providers are shut down in order (traces, metrics, logs)
|
|
45
|
+
- **AND** pending signals are flushed to the exporter
|
|
46
|
+
- **AND** no error is raised if the exporter is unreachable (logged at WARN)
|
|
47
|
+
|
|
48
|
+
#### Scenario: Shutdown before init is a no-op
|
|
49
|
+
- **GIVEN** `telemetry.init()` was never called
|
|
50
|
+
- **WHEN** `telemetry.shutdown()` is called
|
|
51
|
+
- **THEN** no exception is raised
|
carros_ai_utils-0.2.0/openspec/changes/archive/2026-04-03-init-telemetry-python/specs/logger/spec.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
## Purpose
|
|
2
|
+
Bridge the Python standard logging module to OpenTelemetry so that all
|
|
3
|
+
existing logging.getLogger() calls are automatically exported via OTLP,
|
|
4
|
+
with no changes required in application code.
|
|
5
|
+
|
|
6
|
+
### Requirement: Logging Bridge via LoggingHandler
|
|
7
|
+
After `telemetry.init()`, the library SHALL attach an OTel LoggingHandler to
|
|
8
|
+
the Python root logger so all log records at WARNING level and above are
|
|
9
|
+
exported via OTLP.
|
|
10
|
+
|
|
11
|
+
#### Scenario: Existing logger sends to OTLP after init
|
|
12
|
+
- **GIVEN** `telemetry.init("svc")` was called
|
|
13
|
+
- **AND** a logger exists: `logger = logging.getLogger("mymodule")`
|
|
14
|
+
- **WHEN** `logger.warning("something failed")`
|
|
15
|
+
- **THEN** a log record is emitted to the OTel LoggerProvider
|
|
16
|
+
- **AND** no changes to the existing logger code are required
|
|
17
|
+
|
|
18
|
+
#### Scenario: Log level threshold is WARNING
|
|
19
|
+
- **GIVEN** `telemetry.init("svc")` was called
|
|
20
|
+
- **WHEN** `logger.debug("debug message")` is called
|
|
21
|
+
- **THEN** the debug record is NOT exported via OTLP
|
|
22
|
+
- **AND** it MAY still appear in stdout depending on the app's log config
|
|
23
|
+
|
|
24
|
+
#### Scenario: Log record carries trace context
|
|
25
|
+
- **GIVEN** a span is active when a log is emitted
|
|
26
|
+
- **WHEN** `logger.error("payment failed")`
|
|
27
|
+
- **THEN** the exported log record includes `trace_id` and `span_id`
|
|
28
|
+
matching the active span
|
|
29
|
+
- **AND** the log is correlated with the trace in HyperDX
|
|
30
|
+
|
|
31
|
+
### Requirement: No Disruption to Existing Log Handlers
|
|
32
|
+
Attaching the OTel LoggingHandler SHALL NOT remove or replace existing
|
|
33
|
+
handlers on the root logger.
|
|
34
|
+
|
|
35
|
+
#### Scenario: Existing stdout handler preserved
|
|
36
|
+
- **GIVEN** the application has a StreamHandler on the root logger before init()
|
|
37
|
+
- **WHEN** `telemetry.init("svc")` is called
|
|
38
|
+
- **THEN** the StreamHandler is still present and active
|
|
39
|
+
- **AND** logs still appear in stdout as before
|
|
40
|
+
- **AND** logs are also exported via OTLP
|
|
41
|
+
|
|
42
|
+
### Requirement: Stdout Logging in Development
|
|
43
|
+
When not in production mode, the library SHALL NOT attach the OTel
|
|
44
|
+
LoggingHandler — logs remain in stdout only.
|
|
45
|
+
|
|
46
|
+
#### Scenario: No OTLP logging handler in development
|
|
47
|
+
- **GIVEN** `APP_ENVIRONMENT` is not set
|
|
48
|
+
- **WHEN** `telemetry.init("svc")` is called
|
|
49
|
+
- **THEN** no OTel LoggingHandler is added to the root logger
|
|
50
|
+
- **AND** logging behavior is unchanged from default Python behavior
|
carros_ai_utils-0.2.0/openspec/changes/archive/2026-04-03-init-telemetry-python/specs/meter/spec.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
## Purpose
|
|
2
|
+
Provide convenient access to a pre-configured Meter for recording metrics
|
|
3
|
+
(counters, histograms, gauges), without requiring direct interaction with
|
|
4
|
+
the MeterProvider.
|
|
5
|
+
|
|
6
|
+
### Requirement: Meter Access
|
|
7
|
+
The library SHALL expose `telemetry.meter(name?)` that returns an OTel Meter
|
|
8
|
+
scoped to the given name, defaulting to the service name from init().
|
|
9
|
+
|
|
10
|
+
#### Scenario: Meter returned after init
|
|
11
|
+
- **GIVEN** `telemetry.init("payments-api")` was called
|
|
12
|
+
- **WHEN** `meter = telemetry.meter()` is called
|
|
13
|
+
- **THEN** a valid `opentelemetry.metrics.Meter` instance is returned
|
|
14
|
+
|
|
15
|
+
#### Scenario: Counter creation and recording
|
|
16
|
+
- **GIVEN** a valid Meter instance
|
|
17
|
+
- **WHEN** `counter = meter.create_counter("requests_total")` and `counter.add(1)`
|
|
18
|
+
- **THEN** the counter value is buffered for export
|
|
19
|
+
|
|
20
|
+
#### Scenario: Histogram creation and recording
|
|
21
|
+
- **GIVEN** a valid Meter instance
|
|
22
|
+
- **WHEN** `hist = meter.create_histogram("request_duration_ms")` and `hist.record(42.5)`
|
|
23
|
+
- **THEN** the observation is buffered for export
|
|
24
|
+
|
|
25
|
+
#### Scenario: Meter before init returns no-op
|
|
26
|
+
- **GIVEN** `telemetry.init()` was never called
|
|
27
|
+
- **WHEN** `telemetry.meter()` is called
|
|
28
|
+
- **THEN** a no-op Meter is returned
|
|
29
|
+
- **AND** no exception is raised
|
|
30
|
+
|
|
31
|
+
### Requirement: Metric Export Interval
|
|
32
|
+
Metrics SHALL be exported on a periodic basis using the OTLP periodic
|
|
33
|
+
exporting MetricReader with a default interval of 60 seconds.
|
|
34
|
+
|
|
35
|
+
#### Scenario: Default export interval applied
|
|
36
|
+
- **GIVEN** no custom interval is configured
|
|
37
|
+
- **WHEN** the MeterProvider is initialized
|
|
38
|
+
- **THEN** a PeriodicExportingMetricReader with `export_interval_millis=60000` is used
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
## Purpose
|
|
2
|
+
Configure OTLP gRPC exporters for traces, metrics, and logs that point to
|
|
3
|
+
the telemetry-collector, or fall back to stdout exporters for local development.
|
|
4
|
+
|
|
5
|
+
### Requirement: gRPC Exporters in Production
|
|
6
|
+
When `config.is_production` is True, the library SHALL create OTLP gRPC
|
|
7
|
+
exporters for all three signal types using the configured endpoint.
|
|
8
|
+
|
|
9
|
+
#### Scenario: Trace exporter created for production
|
|
10
|
+
- **GIVEN** `APP_ENVIRONMENT=production` and `OTEL_EXPORTER_OTLP_ENDPOINT=collector:4317`
|
|
11
|
+
- **WHEN** the trace exporter is built
|
|
12
|
+
- **THEN** it is an `OTLPSpanExporter` configured with `endpoint="collector:4317"`
|
|
13
|
+
- **AND** it uses insecure credentials (no TLS — private network)
|
|
14
|
+
|
|
15
|
+
#### Scenario: Metrics exporter created for production
|
|
16
|
+
- **GIVEN** `APP_ENVIRONMENT=production`
|
|
17
|
+
- **WHEN** the metrics exporter is built
|
|
18
|
+
- **THEN** it is an `OTLPMetricExporter` configured with the same endpoint
|
|
19
|
+
|
|
20
|
+
#### Scenario: Logs exporter created for production
|
|
21
|
+
- **GIVEN** `APP_ENVIRONMENT=production`
|
|
22
|
+
- **WHEN** the logs exporter is built
|
|
23
|
+
- **THEN** it is an `OTLPLogExporter` configured with the same endpoint
|
|
24
|
+
|
|
25
|
+
### Requirement: Stdout Exporters in Development
|
|
26
|
+
When `config.is_production` is False, the library SHALL use console/stdout
|
|
27
|
+
exporters so developers can inspect signals locally without a collector.
|
|
28
|
+
|
|
29
|
+
#### Scenario: Console exporters used in development
|
|
30
|
+
- **GIVEN** `APP_ENVIRONMENT` is not set (defaults to development)
|
|
31
|
+
- **WHEN** exporters are built
|
|
32
|
+
- **THEN** traces use `ConsoleSpanExporter`
|
|
33
|
+
- **AND** metrics use `ConsoleMetricExporter`
|
|
34
|
+
- **AND** logs use `ConsoleLogExporter`
|
|
35
|
+
- **AND** no network connection is attempted
|
|
36
|
+
|
|
37
|
+
### Requirement: Insecure gRPC Connection
|
|
38
|
+
The OTLP gRPC exporters SHALL connect without TLS when targeting the
|
|
39
|
+
telemetry-collector on the private Docker network.
|
|
40
|
+
|
|
41
|
+
#### Scenario: Insecure credentials used
|
|
42
|
+
- **GIVEN** `APP_ENVIRONMENT=production`
|
|
43
|
+
- **WHEN** the gRPC exporter is initialized
|
|
44
|
+
- **THEN** it passes `insecure=True` (or equivalent credential config)
|
|
45
|
+
- **AND** no certificate files are required
|
carros_ai_utils-0.2.0/openspec/changes/archive/2026-04-03-init-telemetry-python/specs/tracer/spec.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
## Purpose
|
|
2
|
+
Provide convenient access to a pre-configured Tracer for creating spans,
|
|
3
|
+
without requiring application code to interact with the TracerProvider directly.
|
|
4
|
+
|
|
5
|
+
### Requirement: Tracer Access
|
|
6
|
+
The library SHALL expose `telemetry.tracer(name?)` that returns an OTel Tracer
|
|
7
|
+
scoped to the given name, defaulting to the service name from init().
|
|
8
|
+
|
|
9
|
+
#### Scenario: Tracer returned after init
|
|
10
|
+
- **GIVEN** `telemetry.init("payments-api")` was called
|
|
11
|
+
- **WHEN** `tracer = telemetry.tracer()` is called
|
|
12
|
+
- **THEN** a valid `opentelemetry.trace.Tracer` instance is returned
|
|
13
|
+
- **AND** it can create spans via `tracer.start_as_current_span("op")`
|
|
14
|
+
|
|
15
|
+
#### Scenario: Named tracer for library scoping
|
|
16
|
+
- **GIVEN** `telemetry.init("payments-api")` was called
|
|
17
|
+
- **WHEN** `tracer = telemetry.tracer("payments.checkout")` is called
|
|
18
|
+
- **THEN** the returned tracer is scoped to `"payments.checkout"`
|
|
19
|
+
|
|
20
|
+
#### Scenario: Tracer before init returns no-op
|
|
21
|
+
- **GIVEN** `telemetry.init()` was never called
|
|
22
|
+
- **WHEN** `telemetry.tracer()` is called
|
|
23
|
+
- **THEN** a no-op Tracer is returned (spans are created but not exported)
|
|
24
|
+
- **AND** no exception is raised
|
|
25
|
+
|
|
26
|
+
### Requirement: Span Context Propagation
|
|
27
|
+
Tracers created via `telemetry.tracer()` SHALL propagate context automatically
|
|
28
|
+
via the W3C TraceContext standard.
|
|
29
|
+
|
|
30
|
+
#### Scenario: Child span linked to parent
|
|
31
|
+
- **GIVEN** a parent span is active
|
|
32
|
+
- **WHEN** a new span is started via `tracer.start_as_current_span("child")`
|
|
33
|
+
- **THEN** the child span carries the same `trace_id` as the parent
|
|
34
|
+
- **AND** its `parent_span_id` equals the parent span's `span_id`
|