plain.connect 0.3.4__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.
- plain_connect-0.3.4/.gitignore +26 -0
- plain_connect-0.3.4/LICENSE +28 -0
- plain_connect-0.3.4/PKG-INFO +96 -0
- plain_connect-0.3.4/README.md +1 -0
- plain_connect-0.3.4/plain/connect/CHANGELOG.md +122 -0
- plain_connect-0.3.4/plain/connect/README.md +83 -0
- plain_connect-0.3.4/plain/connect/__init__.py +3 -0
- plain_connect-0.3.4/plain/connect/config.py +194 -0
- plain_connect-0.3.4/plain/connect/default_settings.py +10 -0
- plain_connect-0.3.4/pyproject.toml +23 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.venv
|
|
2
|
+
/.env
|
|
3
|
+
*.egg-info
|
|
4
|
+
*.py[co]
|
|
5
|
+
__pycache__
|
|
6
|
+
*.DS_Store
|
|
7
|
+
*.swp
|
|
8
|
+
*.swo
|
|
9
|
+
|
|
10
|
+
/*.code-workspace
|
|
11
|
+
|
|
12
|
+
# Test apps
|
|
13
|
+
plain*/tests/.plain
|
|
14
|
+
|
|
15
|
+
# Agent scratch files
|
|
16
|
+
/scratch
|
|
17
|
+
|
|
18
|
+
# Plain temp dirs
|
|
19
|
+
.plain
|
|
20
|
+
|
|
21
|
+
.vscode
|
|
22
|
+
/.claude/settings.local.json
|
|
23
|
+
/.claude/skills/announcements/
|
|
24
|
+
/CLAUDE.local.md
|
|
25
|
+
/.benchmarks
|
|
26
|
+
.claude/worktrees
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Dropseed, LLC
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plain.connect
|
|
3
|
+
Version: 0.3.4
|
|
4
|
+
Summary: Connect your Plain app to Plain Cloud via OTLP export.
|
|
5
|
+
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.13
|
|
9
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.34.1
|
|
10
|
+
Requires-Dist: opentelemetry-sdk>=1.34.1
|
|
11
|
+
Requires-Dist: plain<1.0.0,>=0.113.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# plain.connect
|
|
15
|
+
|
|
16
|
+
**Connect your Plain app to Plain Cloud via OTLP export.**
|
|
17
|
+
|
|
18
|
+
- [Overview](#overview)
|
|
19
|
+
- [Settings](#settings)
|
|
20
|
+
- [Sampling](#sampling)
|
|
21
|
+
- [What gets exported](#what-gets-exported)
|
|
22
|
+
- [Observer coexistence](#observer-coexistence)
|
|
23
|
+
- [FAQs](#faqs)
|
|
24
|
+
- [Installation](#installation)
|
|
25
|
+
|
|
26
|
+
## Overview
|
|
27
|
+
|
|
28
|
+
You can use plain.connect to export traces, metrics, and logs from your Plain app to Plain Cloud. The framework already instruments itself with OpenTelemetry spans and histograms — plain.connect activates them by providing the OTLP exporters and bridges Python's `logging` module into OTLP log records.
|
|
29
|
+
|
|
30
|
+
Set one environment variable and your app starts pushing telemetry:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
PLAIN_CONNECT_EXPORT_TOKEN=your-token
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
If `CONNECT_EXPORT_TOKEN` is not set, the package is a no-op — safe to install without configuration.
|
|
37
|
+
|
|
38
|
+
## Settings
|
|
39
|
+
|
|
40
|
+
| Setting | Default | Description |
|
|
41
|
+
| --------------------------- | ------------------------------------- | ----------------------------------------------------------- |
|
|
42
|
+
| `CONNECT_EXPORT_URL` | `"https://ingest.plainframework.com"` | OTLP ingest endpoint (override to use a custom endpoint) |
|
|
43
|
+
| `CONNECT_EXPORT_TOKEN` | `""` | Auth token for the export endpoint |
|
|
44
|
+
| `CONNECT_TRACE_SAMPLE_RATE` | `1.0` | Probability of exporting a trace (0.0–1.0) |
|
|
45
|
+
| `CONNECT_EXPORT_LOGS` | `True` | Set to `False` to disable OTLP log export |
|
|
46
|
+
| `CONNECT_LOG_LEVEL` | `"INFO"` | Minimum severity exported via OTLP logs (level name or int) |
|
|
47
|
+
|
|
48
|
+
All settings can be set via `PLAIN_`-prefixed environment variables or in `app/settings.py`.
|
|
49
|
+
|
|
50
|
+
## Sampling
|
|
51
|
+
|
|
52
|
+
By default, all traces are exported. To reduce volume, set a sample rate:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
CONNECT_TRACE_SAMPLE_RATE = 0.1 # Export 10% of traces
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Metrics are not affected by sampling — histograms aggregate in-process and export periodically regardless of the trace sample rate.
|
|
59
|
+
|
|
60
|
+
## What gets exported
|
|
61
|
+
|
|
62
|
+
**Traces** — HTTP request spans and database query spans instrumented by the framework.
|
|
63
|
+
|
|
64
|
+
**Metrics** — OTel histograms like `db.client.query.duration`, aggregated and pushed every 60 seconds.
|
|
65
|
+
|
|
66
|
+
**Logs** — Records from the `plain` and `app` loggers, plus anything propagating to the root logger, are bridged into OTLP log records and exported with `trace_id` / `span_id` set from the active span. The minimum severity is controlled by `CONNECT_LOG_LEVEL` (default `INFO`); the root logger's level is widened to that floor when needed so libraries using `getLogger(__name__)` reach the exporter. To prevent feedback loops, two sources are skipped on the export path: the `opentelemetry` namespace, and any record emitted from inside the OTLP exporter's background thread (e.g. urllib3 connection errors raised by the exporter's own HTTP call). Your application's urllib3 logs are exported normally.
|
|
67
|
+
|
|
68
|
+
## Observer coexistence
|
|
69
|
+
|
|
70
|
+
If [plain.observer](../../plain-observer/plain/observer/README.md) is also installed, both work simultaneously. plain.connect handles production export while observer provides the local dev toolbar and admin trace viewer. Observer detects the existing TracerProvider and layers its sampler and span processor on top.
|
|
71
|
+
|
|
72
|
+
## FAQs
|
|
73
|
+
|
|
74
|
+
#### Do I need plain.observer to use plain.connect?
|
|
75
|
+
|
|
76
|
+
No. plain.connect works independently. Observer is for local dev tooling; plain.connect is for production export.
|
|
77
|
+
|
|
78
|
+
#### What happens if the export endpoint is unreachable?
|
|
79
|
+
|
|
80
|
+
The OTLP exporters batch and retry automatically. If the endpoint is down, telemetry is dropped after retries — it does not block your application.
|
|
81
|
+
|
|
82
|
+
#### Does this add latency to requests?
|
|
83
|
+
|
|
84
|
+
No. Trace spans are exported in a background thread via `BatchSpanProcessor`. Metrics are flushed periodically by a background thread. Neither blocks request handling.
|
|
85
|
+
|
|
86
|
+
## Installation
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# app/settings.py
|
|
90
|
+
INSTALLED_PACKAGES = [
|
|
91
|
+
"plain.connect",
|
|
92
|
+
# ...
|
|
93
|
+
]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Place `plain.connect` **before** `plain.observer` in `INSTALLED_PACKAGES` so it sets up the TracerProvider first.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
plain/connect/README.md
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# plain-connect changelog
|
|
2
|
+
|
|
3
|
+
## [0.3.4](https://github.com/dropseed/plain/releases/plain-connect@0.3.4) (2026-05-07)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- **Renamed `plain-cloud` to `plain-connect`.** The package, module path, and config label all change: `plain.cloud` → `plain.connect`, and the package label `plaincloud` → `plainconnect`. All settings move from the `CLOUD_*` prefix to `CONNECT_*` (e.g. `CLOUD_EXPORT_TOKEN` → `CONNECT_EXPORT_TOKEN`, `PLAIN_CLOUD_EXPORT_TOKEN` → `PLAIN_CONNECT_EXPORT_TOKEN`). The destination service is still Plain Cloud — `plain-connect` is the app integration package that ships telemetry to it. ([304fc185cc](https://github.com/dropseed/plain/commit/304fc185cc))
|
|
8
|
+
|
|
9
|
+
### Upgrade instructions
|
|
10
|
+
|
|
11
|
+
- Replace `plain-cloud` with `plain-connect` in your dependencies (e.g. `pyproject.toml`).
|
|
12
|
+
- In `app/settings.py`, replace `"plain.cloud"` with `"plain.connect"` in `INSTALLED_PACKAGES`.
|
|
13
|
+
- Rename any `CLOUD_*` settings to `CONNECT_*`, and any `PLAIN_CLOUD_*` env vars to `PLAIN_CONNECT_*`.
|
|
14
|
+
|
|
15
|
+
## [0.3.3](https://github.com/dropseed/plain/releases/plain-connect@0.3.3) (2026-05-05)
|
|
16
|
+
|
|
17
|
+
### What's changed
|
|
18
|
+
|
|
19
|
+
- Exposes `__version__` from `importlib.metadata` on `plain.cloud` for version probes that don't want to scrape pip metadata. ([c6cf6edb](https://github.com/dropseed/plain/commit/c6cf6edb))
|
|
20
|
+
|
|
21
|
+
### Upgrade instructions
|
|
22
|
+
|
|
23
|
+
- No changes required.
|
|
24
|
+
|
|
25
|
+
## [0.3.2](https://github.com/dropseed/plain/releases/plain-connect@0.3.2) (2026-04-30)
|
|
26
|
+
|
|
27
|
+
### What's changed
|
|
28
|
+
|
|
29
|
+
- **Suppressed Sentry capture for OTLP exporter batch failures.** The OpenTelemetry SDK's exporters log `"Failed to export X batch"` at ERROR after retries are exhausted, which Sentry's `LoggingIntegration` would otherwise turn into an issue per app per incident — noise the app owner can't act on (network/edge timeouts, ingest backend hiccups). The records still flow to console/file/etc.; only the Sentry capture is suppressed. Mirrors the Sentry SDK's own self-protection for `sentry_sdk.errors` and `urllib3.connectionpool`. ([eb771d82d2de](https://github.com/dropseed/plain/commit/eb771d82d2de))
|
|
30
|
+
|
|
31
|
+
### Upgrade instructions
|
|
32
|
+
|
|
33
|
+
- No changes required.
|
|
34
|
+
|
|
35
|
+
## [0.3.1](https://github.com/dropseed/plain/releases/plain-connect@0.3.1) (2026-04-28)
|
|
36
|
+
|
|
37
|
+
### What's changed
|
|
38
|
+
|
|
39
|
+
- The OTLP span, metric, and log exporters now use gzip compression and a 30-second timeout, reducing egress bandwidth and giving slow ingest endpoints more headroom before requests are dropped. ([891864bcf710](https://github.com/dropseed/plain/commit/891864bcf710))
|
|
40
|
+
|
|
41
|
+
### Upgrade instructions
|
|
42
|
+
|
|
43
|
+
- No changes required.
|
|
44
|
+
|
|
45
|
+
## [0.3.0](https://github.com/dropseed/plain/releases/plain-connect@0.3.0) (2026-04-27)
|
|
46
|
+
|
|
47
|
+
### What's changed
|
|
48
|
+
|
|
49
|
+
- **Added OTLP log export.** Records from the `plain` and `app` loggers, plus anything propagating to the root logger, are bridged into OTLP log records and exported alongside traces and metrics, with `trace_id` / `span_id` populated from the active span. Two new settings: `CLOUD_EXPORT_LOGS` (default `True`) and `CLOUD_LOG_LEVEL` (default `"INFO"`, accepts a level name or int). The root logger's effective level is widened upward to `CLOUD_LOG_LEVEL` when narrower so libraries using `getLogger(__name__)` reach the exporter; it is never narrowed. To prevent feedback loops under transport failure, the exporter ignores records from the `opentelemetry` namespace and from any OTel SDK exporter thread (`OtelBatchSpanRecordProcessor`, `OtelBatchLogRecordProcessor`, `OtelPeriodicExportingMetricReader`). Application urllib3 logs are exported normally. ([3937adee2153](https://github.com/dropseed/plain/commit/3937adee2153))
|
|
50
|
+
- Added a `LoggerProvider` collision check that mirrors the existing `TracerProvider` check, so `plain.cloud` will fail loudly with the "list before plain.observer" message if another package has already installed a logger provider. ([3937adee2153](https://github.com/dropseed/plain/commit/3937adee2153))
|
|
51
|
+
|
|
52
|
+
### Upgrade instructions
|
|
53
|
+
|
|
54
|
+
- No changes required. To opt out of log export, set `CLOUD_EXPORT_LOGS=False` (or `PLAIN_CLOUD_EXPORT_LOGS=false`). To raise/lower the severity floor, set `CLOUD_LOG_LEVEL` (e.g. `"WARNING"`).
|
|
55
|
+
|
|
56
|
+
## [0.2.0](https://github.com/dropseed/plain/releases/plain-connect@0.2.0) (2026-04-27)
|
|
57
|
+
|
|
58
|
+
### What's changed
|
|
59
|
+
|
|
60
|
+
- **Changed the default `CLOUD_EXPORT_URL` to `https://ingest.plainframework.com`** (was `https://plainframework.com/otel`). Projects relying on the default will now export to the dedicated ingest subdomain. ([e58c02eaab9e](https://github.com/dropseed/plain/commit/e58c02eaab9e))
|
|
61
|
+
|
|
62
|
+
### Upgrade instructions
|
|
63
|
+
|
|
64
|
+
- If you were depending on the previous default, set `PLAIN_CLOUD_EXPORT_URL=https://plainframework.com/otel` (or assign `CLOUD_EXPORT_URL` in `app/settings.py`) to keep the old endpoint. Otherwise no changes required.
|
|
65
|
+
|
|
66
|
+
## [0.1.5](https://github.com/dropseed/plain/releases/plain-connect@0.1.5) (2026-04-13)
|
|
67
|
+
|
|
68
|
+
### What's changed
|
|
69
|
+
|
|
70
|
+
- Removed redundant `atexit` shutdown registrations that duplicated the shutdown hooks already registered elsewhere. ([dfb2ce53cd5c](https://github.com/dropseed/plain/commit/dfb2ce53cd5c))
|
|
71
|
+
|
|
72
|
+
### Upgrade instructions
|
|
73
|
+
|
|
74
|
+
- No changes required.
|
|
75
|
+
|
|
76
|
+
## [0.1.4](https://github.com/dropseed/plain/releases/plain-connect@0.1.4) (2026-04-02)
|
|
77
|
+
|
|
78
|
+
### What's changed
|
|
79
|
+
|
|
80
|
+
- Switched metrics export to delta temporality for Counter, Histogram, and UpDownCounter. Each export now contains only the increment since the last collection, making server-side aggregation in ClickHouse straightforward. ([ab431cb5ffe6](https://github.com/dropseed/plain/commit/ab431cb5ffe6))
|
|
81
|
+
|
|
82
|
+
### Upgrade instructions
|
|
83
|
+
|
|
84
|
+
- No changes required.
|
|
85
|
+
|
|
86
|
+
## [0.1.3](https://github.com/dropseed/plain/releases/plain-connect@0.1.3) (2026-04-01)
|
|
87
|
+
|
|
88
|
+
### What's changed
|
|
89
|
+
|
|
90
|
+
- Added `CLOUD_EXPORT_ENABLED` setting (defaults to `True`) to allow disabling all OTEL reporting without removing the token. Set `PLAIN_CLOUD_EXPORT_ENABLED=false` to turn it off. ([e9c4d140b227](https://github.com/dropseed/plain/commit/e9c4d140b227))
|
|
91
|
+
- Raises `RuntimeError` if another tracer provider is already configured when plain.cloud initializes — ensures `plain.cloud` is listed before `plain.observer` in `INSTALLED_PACKAGES`. ([40252d96ce7d](https://github.com/dropseed/plain/commit/40252d96ce7d))
|
|
92
|
+
|
|
93
|
+
### Upgrade instructions
|
|
94
|
+
|
|
95
|
+
- No changes required.
|
|
96
|
+
|
|
97
|
+
## [0.1.2](https://github.com/dropseed/plain/releases/plain-connect@0.1.2) (2026-04-01)
|
|
98
|
+
|
|
99
|
+
### What's changed
|
|
100
|
+
|
|
101
|
+
- `CLOUD_EXPORT_URL` now defaults to `https://plainframework.com/otel` — no need to set it manually. Export is gated on `CLOUD_EXPORT_TOKEN` instead, so only one env var is needed to start pushing telemetry. ([fa711758acda](https://github.com/dropseed/plain/commit/fa711758acda))
|
|
102
|
+
|
|
103
|
+
### Upgrade instructions
|
|
104
|
+
|
|
105
|
+
- If you had `PLAIN_CLOUD_EXPORT_URL` set to `https://plainframework.com/otel`, you can remove it — that's now the default.
|
|
106
|
+
- If you relied on leaving `CLOUD_EXPORT_URL` empty to disable export, set `CLOUD_EXPORT_TOKEN` to empty instead (or just don't set it).
|
|
107
|
+
|
|
108
|
+
## [0.1.1](https://github.com/dropseed/plain/releases/plain-connect@0.1.1) (2026-04-01)
|
|
109
|
+
|
|
110
|
+
### What's changed
|
|
111
|
+
|
|
112
|
+
- Updated export endpoint URLs in docs and default settings from `plaincloud.com` to `plainframework.com/otel`. ([15bb896cdbe6](https://github.com/dropseed/plain/commit/15bb896cdbe6))
|
|
113
|
+
|
|
114
|
+
### Upgrade instructions
|
|
115
|
+
|
|
116
|
+
- If you have `PLAIN_CLOUD_EXPORT_URL` set to `https://ingest.plaincloud.com`, update it to `https://plainframework.com/otel`.
|
|
117
|
+
|
|
118
|
+
## [0.1.0](https://github.com/dropseed/plain/releases/plain-connect@0.1.0) (2026-04-01)
|
|
119
|
+
|
|
120
|
+
### What's changed
|
|
121
|
+
|
|
122
|
+
- **Initial release.** Sets up OpenTelemetry TracerProvider and MeterProvider with OTLP HTTP exporters, pushing traces and metrics to Plain Cloud. Configure with `CLOUD_EXPORT_URL` and `CLOUD_EXPORT_TOKEN` settings. Includes head-based trace sampling via `CLOUD_TRACE_SAMPLE_RATE`. Inactive when `CLOUD_EXPORT_URL` is not set. Coexists with plain-observer — observer layers its sampler and span processor on top. ([e3971506cb](https://github.com/dropseed/plain/commit/e3971506cb))
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# plain.connect
|
|
2
|
+
|
|
3
|
+
**Connect your Plain app to Plain Cloud via OTLP export.**
|
|
4
|
+
|
|
5
|
+
- [Overview](#overview)
|
|
6
|
+
- [Settings](#settings)
|
|
7
|
+
- [Sampling](#sampling)
|
|
8
|
+
- [What gets exported](#what-gets-exported)
|
|
9
|
+
- [Observer coexistence](#observer-coexistence)
|
|
10
|
+
- [FAQs](#faqs)
|
|
11
|
+
- [Installation](#installation)
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
You can use plain.connect to export traces, metrics, and logs from your Plain app to Plain Cloud. The framework already instruments itself with OpenTelemetry spans and histograms — plain.connect activates them by providing the OTLP exporters and bridges Python's `logging` module into OTLP log records.
|
|
16
|
+
|
|
17
|
+
Set one environment variable and your app starts pushing telemetry:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
PLAIN_CONNECT_EXPORT_TOKEN=your-token
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
If `CONNECT_EXPORT_TOKEN` is not set, the package is a no-op — safe to install without configuration.
|
|
24
|
+
|
|
25
|
+
## Settings
|
|
26
|
+
|
|
27
|
+
| Setting | Default | Description |
|
|
28
|
+
| --------------------------- | ------------------------------------- | ----------------------------------------------------------- |
|
|
29
|
+
| `CONNECT_EXPORT_URL` | `"https://ingest.plainframework.com"` | OTLP ingest endpoint (override to use a custom endpoint) |
|
|
30
|
+
| `CONNECT_EXPORT_TOKEN` | `""` | Auth token for the export endpoint |
|
|
31
|
+
| `CONNECT_TRACE_SAMPLE_RATE` | `1.0` | Probability of exporting a trace (0.0–1.0) |
|
|
32
|
+
| `CONNECT_EXPORT_LOGS` | `True` | Set to `False` to disable OTLP log export |
|
|
33
|
+
| `CONNECT_LOG_LEVEL` | `"INFO"` | Minimum severity exported via OTLP logs (level name or int) |
|
|
34
|
+
|
|
35
|
+
All settings can be set via `PLAIN_`-prefixed environment variables or in `app/settings.py`.
|
|
36
|
+
|
|
37
|
+
## Sampling
|
|
38
|
+
|
|
39
|
+
By default, all traces are exported. To reduce volume, set a sample rate:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
CONNECT_TRACE_SAMPLE_RATE = 0.1 # Export 10% of traces
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Metrics are not affected by sampling — histograms aggregate in-process and export periodically regardless of the trace sample rate.
|
|
46
|
+
|
|
47
|
+
## What gets exported
|
|
48
|
+
|
|
49
|
+
**Traces** — HTTP request spans and database query spans instrumented by the framework.
|
|
50
|
+
|
|
51
|
+
**Metrics** — OTel histograms like `db.client.query.duration`, aggregated and pushed every 60 seconds.
|
|
52
|
+
|
|
53
|
+
**Logs** — Records from the `plain` and `app` loggers, plus anything propagating to the root logger, are bridged into OTLP log records and exported with `trace_id` / `span_id` set from the active span. The minimum severity is controlled by `CONNECT_LOG_LEVEL` (default `INFO`); the root logger's level is widened to that floor when needed so libraries using `getLogger(__name__)` reach the exporter. To prevent feedback loops, two sources are skipped on the export path: the `opentelemetry` namespace, and any record emitted from inside the OTLP exporter's background thread (e.g. urllib3 connection errors raised by the exporter's own HTTP call). Your application's urllib3 logs are exported normally.
|
|
54
|
+
|
|
55
|
+
## Observer coexistence
|
|
56
|
+
|
|
57
|
+
If [plain.observer](../../plain-observer/plain/observer/README.md) is also installed, both work simultaneously. plain.connect handles production export while observer provides the local dev toolbar and admin trace viewer. Observer detects the existing TracerProvider and layers its sampler and span processor on top.
|
|
58
|
+
|
|
59
|
+
## FAQs
|
|
60
|
+
|
|
61
|
+
#### Do I need plain.observer to use plain.connect?
|
|
62
|
+
|
|
63
|
+
No. plain.connect works independently. Observer is for local dev tooling; plain.connect is for production export.
|
|
64
|
+
|
|
65
|
+
#### What happens if the export endpoint is unreachable?
|
|
66
|
+
|
|
67
|
+
The OTLP exporters batch and retry automatically. If the endpoint is down, telemetry is dropped after retries — it does not block your application.
|
|
68
|
+
|
|
69
|
+
#### Does this add latency to requests?
|
|
70
|
+
|
|
71
|
+
No. Trace spans are exported in a background thread via `BatchSpanProcessor`. Metrics are flushed periodically by a background thread. Neither blocks request handling.
|
|
72
|
+
|
|
73
|
+
## Installation
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# app/settings.py
|
|
77
|
+
INSTALLED_PACKAGES = [
|
|
78
|
+
"plain.connect",
|
|
79
|
+
# ...
|
|
80
|
+
]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Place `plain.connect` **before** `plain.observer` in `INSTALLED_PACKAGES` so it sets up the TracerProvider first.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
from opentelemetry import _logs, metrics, trace
|
|
7
|
+
from opentelemetry._logs._internal import ProxyLoggerProvider
|
|
8
|
+
from opentelemetry.exporter.otlp.proto.http import Compression
|
|
9
|
+
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
|
|
10
|
+
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
|
|
11
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
12
|
+
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
|
|
13
|
+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
|
14
|
+
from opentelemetry.sdk.metrics import Counter, Histogram, MeterProvider, UpDownCounter
|
|
15
|
+
from opentelemetry.sdk.metrics.export import (
|
|
16
|
+
AggregationTemporality,
|
|
17
|
+
PeriodicExportingMetricReader,
|
|
18
|
+
)
|
|
19
|
+
from opentelemetry.sdk.resources import Resource
|
|
20
|
+
from opentelemetry.sdk.trace import TracerProvider, sampling
|
|
21
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
22
|
+
from opentelemetry.semconv.attributes import service_attributes
|
|
23
|
+
|
|
24
|
+
from plain.packages import PackageConfig, register_config
|
|
25
|
+
from plain.runtime import settings
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _ExporterLoopFilter(logging.Filter):
|
|
29
|
+
"""Block records that would feed back into OTLP export under failure.
|
|
30
|
+
|
|
31
|
+
Two sources to suppress:
|
|
32
|
+
|
|
33
|
+
1. The OpenTelemetry SDK's own namespace — its `failed to export`
|
|
34
|
+
warnings would re-queue indefinitely.
|
|
35
|
+
2. Anything emitted from inside an OTel SDK exporter thread (e.g.
|
|
36
|
+
urllib3 connection errors raised by the OTLP HTTP exporter). The
|
|
37
|
+
SDK names every exporter thread with an `Otel` prefix —
|
|
38
|
+
`OtelBatch{Span,Log}RecordProcessor` for traces/logs, and
|
|
39
|
+
`OtelPeriodicExportingMetricReader` for metrics — so matching on
|
|
40
|
+
that prefix catches all three. Scoping to the thread, not the
|
|
41
|
+
urllib3 namespace, lets user-code urllib3 logs flow through.
|
|
42
|
+
|
|
43
|
+
See `opentelemetry/sdk/_shared_internal/__init__.py` (BatchProcessor)
|
|
44
|
+
and `opentelemetry/sdk/metrics/export/__init__.py`
|
|
45
|
+
(PeriodicExportingMetricReader) for the thread names.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
49
|
+
name = record.name
|
|
50
|
+
if name == "opentelemetry" or name.startswith("opentelemetry."):
|
|
51
|
+
return False
|
|
52
|
+
if threading.current_thread().name.startswith("Otel"):
|
|
53
|
+
return False
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@register_config
|
|
58
|
+
class Config(PackageConfig):
|
|
59
|
+
package_label = "plainconnect"
|
|
60
|
+
|
|
61
|
+
def ready(self) -> None:
|
|
62
|
+
if not settings.CONNECT_EXPORT_ENABLED or not settings.CONNECT_EXPORT_TOKEN:
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
# Don't capture per-batch OTLP export failures as Sentry events. The OTel
|
|
66
|
+
# SDK's exporters log "Failed to export X batch" at ERROR after retries
|
|
67
|
+
# are exhausted, which Sentry's LoggingIntegration would otherwise turn
|
|
68
|
+
# into an issue per app per incident — noise the app owner can't act on
|
|
69
|
+
# (network/edge timeouts, ingest backend hiccups). The records still
|
|
70
|
+
# flow to console/file/etc. — only the Sentry capture is suppressed.
|
|
71
|
+
# Mirrors Sentry SDK's own self-protection for `sentry_sdk.errors` and
|
|
72
|
+
# `urllib3.connectionpool` in sentry_sdk/integrations/logging.py.
|
|
73
|
+
try:
|
|
74
|
+
import sentry_sdk.integrations.logging as _sentry_log # ty: ignore[unresolved-import]
|
|
75
|
+
except ImportError:
|
|
76
|
+
pass
|
|
77
|
+
else:
|
|
78
|
+
for name in (
|
|
79
|
+
"opentelemetry.exporter.otlp.proto.http.trace_exporter",
|
|
80
|
+
"opentelemetry.exporter.otlp.proto.http.metric_exporter",
|
|
81
|
+
"opentelemetry.exporter.otlp.proto.http._log_exporter",
|
|
82
|
+
"opentelemetry.sdk._shared_internal",
|
|
83
|
+
"opentelemetry.sdk._logs._internal.export",
|
|
84
|
+
"opentelemetry.sdk.metrics._internal.export",
|
|
85
|
+
):
|
|
86
|
+
_sentry_log.ignore_logger(name)
|
|
87
|
+
|
|
88
|
+
resource = Resource.create(
|
|
89
|
+
{
|
|
90
|
+
service_attributes.SERVICE_NAME: settings.NAME,
|
|
91
|
+
service_attributes.SERVICE_VERSION: settings.VERSION,
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
export_url = str(settings.CONNECT_EXPORT_URL).rstrip("/")
|
|
96
|
+
headers = {"Authorization": f"Bearer {settings.CONNECT_EXPORT_TOKEN}"}
|
|
97
|
+
|
|
98
|
+
# Traces
|
|
99
|
+
current_provider = trace.get_tracer_provider()
|
|
100
|
+
if current_provider and not isinstance(
|
|
101
|
+
current_provider, trace.ProxyTracerProvider
|
|
102
|
+
):
|
|
103
|
+
raise RuntimeError(
|
|
104
|
+
"A tracer provider already exists."
|
|
105
|
+
" plain.connect must be listed before plain.observer in INSTALLED_PACKAGES."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
span_exporter = OTLPSpanExporter(
|
|
109
|
+
endpoint=f"{export_url}/v1/traces",
|
|
110
|
+
headers=headers,
|
|
111
|
+
timeout=30,
|
|
112
|
+
compression=Compression.Gzip,
|
|
113
|
+
)
|
|
114
|
+
sampler = sampling.TraceIdRatioBased(settings.CONNECT_TRACE_SAMPLE_RATE)
|
|
115
|
+
tracer_provider = TracerProvider(sampler=sampler, resource=resource)
|
|
116
|
+
tracer_provider.add_span_processor(BatchSpanProcessor(span_exporter))
|
|
117
|
+
trace.set_tracer_provider(tracer_provider)
|
|
118
|
+
|
|
119
|
+
# Metrics — use delta temporality so each export contains only the
|
|
120
|
+
# increment since the last export, not a running total. This makes
|
|
121
|
+
# server-side aggregation (sum/avg in ClickHouse) straightforward.
|
|
122
|
+
metric_exporter = OTLPMetricExporter(
|
|
123
|
+
endpoint=f"{export_url}/v1/metrics",
|
|
124
|
+
headers=headers,
|
|
125
|
+
timeout=30,
|
|
126
|
+
compression=Compression.Gzip,
|
|
127
|
+
preferred_temporality={
|
|
128
|
+
Counter: AggregationTemporality.DELTA,
|
|
129
|
+
Histogram: AggregationTemporality.DELTA,
|
|
130
|
+
UpDownCounter: AggregationTemporality.DELTA,
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
reader = PeriodicExportingMetricReader(metric_exporter)
|
|
134
|
+
meter_provider = MeterProvider(metric_readers=[reader], resource=resource)
|
|
135
|
+
metrics.set_meter_provider(meter_provider)
|
|
136
|
+
|
|
137
|
+
# Logs
|
|
138
|
+
if settings.CONNECT_EXPORT_LOGS:
|
|
139
|
+
current_logger_provider = _logs.get_logger_provider()
|
|
140
|
+
if current_logger_provider and not isinstance(
|
|
141
|
+
current_logger_provider, ProxyLoggerProvider
|
|
142
|
+
):
|
|
143
|
+
raise RuntimeError(
|
|
144
|
+
"A logger provider already exists."
|
|
145
|
+
" plain.connect must be listed before plain.observer in INSTALLED_PACKAGES."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Accept either a level name ("INFO") or an int (20).
|
|
149
|
+
raw_level = settings.CONNECT_LOG_LEVEL
|
|
150
|
+
if isinstance(raw_level, str):
|
|
151
|
+
log_level = logging.getLevelName(raw_level.upper())
|
|
152
|
+
if not isinstance(log_level, int):
|
|
153
|
+
raise ValueError(
|
|
154
|
+
f"CONNECT_LOG_LEVEL={raw_level!r} is not a valid logging level."
|
|
155
|
+
)
|
|
156
|
+
else:
|
|
157
|
+
log_level = int(raw_level)
|
|
158
|
+
|
|
159
|
+
log_exporter = OTLPLogExporter(
|
|
160
|
+
endpoint=f"{export_url}/v1/logs",
|
|
161
|
+
headers=headers,
|
|
162
|
+
timeout=30,
|
|
163
|
+
compression=Compression.Gzip,
|
|
164
|
+
)
|
|
165
|
+
logger_provider = LoggerProvider(resource=resource)
|
|
166
|
+
logger_provider.add_log_record_processor(
|
|
167
|
+
BatchLogRecordProcessor(log_exporter)
|
|
168
|
+
)
|
|
169
|
+
_logs.set_logger_provider(logger_provider)
|
|
170
|
+
|
|
171
|
+
handler = LoggingHandler(level=log_level, logger_provider=logger_provider)
|
|
172
|
+
# Filter on the handler (not the loggers) so OTLP exporter and
|
|
173
|
+
# HTTP-client diagnostics still reach the app's console/file
|
|
174
|
+
# handlers — we only stop them from being re-exported, which
|
|
175
|
+
# would loop under failure.
|
|
176
|
+
handler.addFilter(_ExporterLoopFilter())
|
|
177
|
+
|
|
178
|
+
# Plain's `configure_logging` sets `plain` and `app` to
|
|
179
|
+
# propagate=False, so attaching only to root would miss them.
|
|
180
|
+
# Attach to root for everything else (user `getLogger(__name__)`,
|
|
181
|
+
# third-party libs).
|
|
182
|
+
for name in ("", "plain", "app"):
|
|
183
|
+
logging.getLogger(name).addHandler(handler)
|
|
184
|
+
|
|
185
|
+
# Root defaults to WARNING. A library that uses
|
|
186
|
+
# `logging.getLogger(__name__)` without setting its own level
|
|
187
|
+
# inherits root's effective level — so INFO/DEBUG records get
|
|
188
|
+
# dropped before the OTLP handler runs. Widen root just enough
|
|
189
|
+
# to let CONNECT_LOG_LEVEL through; never narrow it.
|
|
190
|
+
# NOTSET (0) on root already means "all messages processed",
|
|
191
|
+
# so leave it alone in that case.
|
|
192
|
+
root = logging.getLogger()
|
|
193
|
+
if root.level != logging.NOTSET and root.level > log_level:
|
|
194
|
+
root.setLevel(log_level)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from plain.runtime import Secret
|
|
2
|
+
|
|
3
|
+
CONNECT_EXPORT_ENABLED: bool = True # Set to False to disable all OTEL reporting
|
|
4
|
+
CONNECT_EXPORT_URL: str = "https://ingest.plainframework.com"
|
|
5
|
+
CONNECT_EXPORT_TOKEN: Secret[str] = "" # Auth token for the export endpoint
|
|
6
|
+
CONNECT_TRACE_SAMPLE_RATE: float = 1.0 # 0.0–1.0, probability of exporting a trace
|
|
7
|
+
CONNECT_EXPORT_LOGS: bool = True # Set to False to disable OTLP log export
|
|
8
|
+
# Minimum severity exported via OTLP logs. Accepts a level name ("INFO",
|
|
9
|
+
# "DEBUG", ...) or the integer level value.
|
|
10
|
+
CONNECT_LOG_LEVEL: str = "INFO"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "plain.connect"
|
|
3
|
+
version = "0.3.4"
|
|
4
|
+
description = "Connect your Plain app to Plain Cloud via OTLP export."
|
|
5
|
+
authors = [{ name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev" }]
|
|
6
|
+
license = "BSD-3-Clause"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.13"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"plain>=0.113.0,<1.0.0",
|
|
11
|
+
"opentelemetry-sdk>=1.34.1",
|
|
12
|
+
"opentelemetry-exporter-otlp-proto-http>=1.34.1",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[dependency-groups]
|
|
16
|
+
dev = ["plain.pytest<1.0.0"]
|
|
17
|
+
|
|
18
|
+
[tool.hatch.build.targets.wheel]
|
|
19
|
+
packages = ["plain"]
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["hatchling"]
|
|
23
|
+
build-backend = "hatchling.build"
|