Metadata-Version: 2.4
Name: pontem
Version: 0.6.0
Summary: Pontem edge SDK — logs, metrics, and config for edge devices
License: Apache-2.0
License-File: LICENSE
Keywords: edge,telemetry,metrics,logging,iot
Author: Pontem AI, Inc.
Author-email: support@pontem.ai
Requires-Python: >=3.9
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: System :: Monitoring
Classifier: Typing :: Typed
Project-URL: Homepage, https://pontemai.com
Project-URL: Repository, https://github.com/pontemai/pontem-python
Description-Content-Type: text/markdown

# Pontem Python SDK

Logs, metrics, and config for processes running on Pontem-managed edge devices. Zero runtime dependencies — stdlib only.

## Requirements

- Python 3.9+.

## Install

```bash
pip install pontem
```

## Quick start

```python
import pontem

pontem.init(service_name="my-service")

pontem.logger.info("model loaded", model="scoring_v3")

pontem.metrics.count("frames_processed")
with pontem.metrics.timer("model.inference"):
    result = model.predict(frame)

pontem.shutdown()  # also runs at process exit
```

That's it. Logs land as JSONL under `/var/lib/pontem/services/<service_name>/logs/` for the agent to ship; metrics POST to a local OpenTelemetry collector. See [Metrics → Setup](#setup) for the collector requirement.

> **Already using stdlib `logging`?** Pass `stdlib_logging=True` to `init` and your existing `logging.getLogger(...).info(...)` calls produce Pontem records — no call-site changes. See [Use with stdlib `logging`](#use-with-stdlib-logging) below.

---

## Logging

OTel-aligned severity levels:

| Method  | OTel SeverityNumber | When to use                            |
|---------|---------------------|----------------------------------------|
| `trace` | 1                   | Fine-grained debugging                 |
| `debug` | 5                   | Diagnostic information                 |
| `info`  | 9                   | Normal operational events              |
| `warn`  | 13                  | Unexpected but recoverable             |
| `error` | 17                  | Errors that need attention             |
| `fatal` | 21                  | Unrecoverable failures                 |

The enum is at `pontem.log.Level` (`Level.TRACE`, `Level.INFO`, …).

### Direct API

```python
pontem.logger.info("model loaded", model="scoring_v3")
pontem.logger.warn("high latency", latency_ms=120, endpoint="/predict")
pontem.logger.error("inference failed", error=str(e), frame_id=frame.id)
```

Keyword arguments become structured attributes. Calls are non-blocking and queued; serialization and disk I/O run on the background thread.

This is what you want on hot paths.

### Use with stdlib `logging`

If your code already uses `logging.getLogger(...).info(...)`, enable the drop-in path:

```python
import logging
import pontem

logging.basicConfig(level=logging.INFO)            # 1. set up your handlers first
pontem.init(service_name="my-service",             # 2. then init with the flag
            stdlib_logging=True)

logging.getLogger(__name__).info("model loaded", extra={"model": "v3"})
```

What it does: installs `PontemFormatter` on every handler currently on the root logger. Destinations, rotation policies, and filters stay intact — only the on-the-wire format changes.

**Call order matters.** The flag swaps formatters on handlers attached to root *at the time of `init`*. Run `basicConfig` / `dictConfig` / `addHandler` first; otherwise `init` raises. Handlers added after `init` are not picked up automatically — call `PontemFormatter.install()` again to apply to them.

You can mix paths freely: `pontem.logger.*` on hot inference loops, stdlib elsewhere. Both produce the same wire shape.

The SDK's own logs (`pontem.sdk`, `pontem.emit`, …) propagate to your chain too. Quiet them with stdlib mechanisms:

```python
logging.getLogger("pontem").setLevel(logging.WARNING)  # WARN+ only
logging.getLogger("pontem").propagate = False          # drop entirely
```

### Custom formatter setup

When you need finer control than the `stdlib_logging=True` flag — e.g. attaching the formatter to specific handlers, or wiring it through `dictConfig`:

```python
from logging.handlers import RotatingFileHandler
from pontem.log import PontemFormatter

handler = RotatingFileHandler("/var/log/myapp/app.log", maxBytes=10_000_000)
handler.setFormatter(PontemFormatter(service_name="my-service"))
logging.getLogger().addHandler(handler)
```

`dictConfig` (YAML / JSON):

```yaml
formatters:
  pontem:
    (): pontem.log.PontemFormatter
    service_name: my-service
handlers:
  console:
    class: logging.StreamHandler
    formatter: pontem
root:
  handlers: [console]
  level: INFO
```

Resource attributes (`service.name`, `service.version`) come from constructor kwargs first, falling back to whatever `pontem.init()` populated. `service.name` is required from at least one source.

**For both formatter paths:**

- **Set the root level.** Stdlib defaults to `WARNING`; `info`/`debug` records are filtered before reaching any handler. `basicConfig(level=logging.INFO)` is the standard fix.
- **No emit pipeline.** Records flow through your handler's I/O (sync `FileHandler`, etc.), not through Pontem's bounded queue + background writer. For non-blocking, queued, rotation-and-gzip behavior on hot paths, use the direct API.

---

## Metrics

Aggregated in memory; the background thread POSTs OTLP/HTTP/JSON batches to a local OpenTelemetry collector every second. All public methods return in under 1µs.

### Setup

Metrics need a collector listening on the device — install `pontem-log-collector` (apt) or the Helm chart for K3s. The collector accepts OTLP/HTTP on port 4318 by default and ships to per-tenant Cloud Monitoring.

The SDK defaults to `http://host.docker.internal:4318` so compose packages just work. Override for host-native or K3s deployments:

```bash
export PONTEM_OTLP_ENDPOINT=http://127.0.0.1:4318                  # host-native
export PONTEM_OTLP_ENDPOINT=http://pontem-log-collector.svc:4318   # K3s
```

The SDK attaches `service` to every datapoint. The collector upserts `device_id` on the way through, so each datapoint reaches GCM with both labels (matching the log-side `labels.service` and `labels.device_id` shape). The metric API doesn't accept caller-supplied labels.

### Counter

Cumulative — each flush reports the running total since process start.

```python
pontem.metrics.count("frames_processed")
pontem.metrics.count("bytes_sent", len(payload))
```

```python
metrics.count(name, amount=1)
```

### Histogram

Cumulative `count`, `sum`, `min`, `max` since process start.

```python
pontem.metrics.record("payload_size", len(data), unit="bytes")
```

```python
metrics.record(name, value, *, unit="")
```

### Gauge

Point-in-time — last write wins per flush interval.

```python
pontem.metrics.set_gauge("gpu_temp", 72.0, unit="celsius")
pontem.metrics.set_gauge("queue_depth", len(queue))
```

```python
metrics.set_gauge(name, value, *, unit="")
```

### Timer

Context manager or decorator. Records elapsed time to a histogram.

```python
with pontem.metrics.timer("model.inference"):
    result = model.predict(frame)

@pontem.metrics.timer("preprocessing")
def preprocess(frame):
    ...
```

```python
metrics.timer(name, *, unit="s")
```

### Reliability

OTLP POSTs that fail (collector down, network blip) buffer to a bounded in-memory retry queue (60 bodies by default, drop-oldest on overflow). The collector's own disk-backed queue covers longer outages. Counter/histogram resets across process restarts are reported with a fresh `start_time` so Cloud Monitoring renders the segments correctly.

### Cardinality

The SDK caps distinct metric names per process at 1000 (configurable via `metric_name_limit=`). Names past the cap are dropped with a one-time warning. Don't generate metric names from user input.

---

## Config

Reads agent-managed values from `/var/lib/pontem/config/<namespace>.json` (or `$PONTEM_CONFIG_DIR`). Each namespace is its own JSON file with a flat `{key: value}` map.

```python
threshold = pontem.config("scoring", "confidence_threshold", default=0.85)
regions = pontem.config("detection", "enabled_regions", default=["all"])

pontem.config.reload()  # call after the agent signals an update
```

`default` is returned when either the namespace file or the key is absent. Files are loaded lazily on first lookup per namespace and cached until `reload()`.

---

## Deployment

### Docker / compose (stdout emit)

For containerized deployments where a sidecar log collector tails stdout, switch the direct API to stdout mode:

```python
pontem.init(service_name="my-service", emit_target="stdout")
```

Or via env var (lets the same image run on edge devices and compose hosts):

```bash
PONTEM_EMIT_TARGET=stdout
```

Precedence: `init(emit_target=...)` > `PONTEM_EMIT_TARGET` > default `"file"`.

In stdout mode, `emit_dir`, file rotation, and gzip compression are no-ops — the docker daemon's `json-file` driver handles container log rotation. This setting affects the direct API only; formatter paths always go through your handler chain.

### File output and rotation

In file mode (default), only logs are written to disk:

```
$PONTEM_EMIT_DIR/
    logs.jsonl                # active (SDK writes)
    logs.jsonl.1713100000.gz  # rotated + compressed (agent picks up + deletes)
```

Files rotate at 10 MB, are gzip-compressed on the background thread, and up to 5 rotated files are kept. Metrics go over HTTP to the collector — see [Metrics → Setup](#setup).

---

## Reference

### `init()`

```python
pontem.init(
    service_name="my-service",      # required — identifies the service in all telemetry
    service_version="1.2.0",        # auto-detected from package metadata if omitted
    emit_dir="/custom/path",        # overrides PONTEM_EMIT_DIR (logs)
    emit_target="file",             # "file" (default) or "stdout"
    stdlib_logging=False,           # True → install PontemFormatter on root handlers
    otlp_endpoint=None,             # overrides PONTEM_OTLP_ENDPOINT (metrics)
    metric_name_limit=1000,         # max distinct metric names per process
    metric_otlp_queue_size=60,      # max buffered failed POSTs (drop-oldest)
)
```

`init()` kwargs take precedence over environment variables. Call once at startup. Device identity (`device_id`) is set on the device by `pontem-log-collector` from `/etc/pontem/device-id`; it isn't a kwarg.

### `shutdown()`

Flushes aggregated metrics, drains the log queue, and closes files. Registered automatically via `atexit`; call explicitly if you need a deterministic flush.

### Environment variables

| Variable                | Purpose                            | Default                              |
|-------------------------|------------------------------------|--------------------------------------|
| `PONTEM_EMIT_DIR`       | Log JSONL output directory         | `/var/lib/pontem/services/<service_name>/logs` |
| `PONTEM_EMIT_TARGET`    | `"file"` or `"stdout"` (logs only) | `"file"`                             |
| `PONTEM_OTLP_ENDPOINT`  | Collector endpoint for metrics     | `http://host.docker.internal:4318`   |
| `PONTEM_CONFIG_DIR`     | Directory containing per-namespace `<namespace>.json` files | `/var/lib/pontem/config`             |

### Wire format

Log record:

```json
{
  "timestamp": "2025-01-15T10:30:00.123456Z",
  "severityNumber": 9,
  "severityText": "INFO",
  "body": "model loaded",
  "attributes": {"model": "scoring_v3"},
  "resource": {"service.name": "my-service"}
}
```

Metrics post as OTLP/JSON `ExportMetricsServiceRequest` bodies — `resourceMetrics → scopeMetrics → metrics[]` with cumulative counters/histograms and point-in-time gauges. Every datapoint carries a `service` attribute; the pontem-log-collector upserts `device_id` on the way through. The full wire shape and proto3 canonical JSON conventions live in [SCHEMA.md](./SCHEMA.md).

---

## Troubleshooting

**No logs on disk.** Confirm `pontem.init()` runs before any `logger` call. Check the emit directory exists and is writable: `ls -la /var/lib/pontem/services/<service_name>/logs/`.

**No metrics in Cloud Monitoring.** Confirm a collector is reachable at `$PONTEM_OTLP_ENDPOINT` (default `http://host.docker.internal:4318`). On a compose customer, also confirm the service has `extra_hosts: ["host.docker.internal:host-gateway"]` (the agent injects this on Pontem-managed compose packages). Check the SDK's logs for `pontem.sdk.metrics` warnings about the name-limit cap or failed POSTs.

**`pontem.config(...)` always returns `default`.** Confirm the agent has written the namespace file: `cat /var/lib/pontem/config/<namespace>.json`. Verify `namespace` matches the filename (without `.json`) and `key` matches a top-level key in that file. Call `pontem.config.reload()` after the agent updates the file.

**`info`/`debug` logs missing from console output.** Stdlib's default level is `WARNING`. Lower it: `logging.basicConfig(level=logging.DEBUG)`.

**Memory creeping up.** You're probably generating metric names from user input or unbounded sources. Each unique metric name is a separate aggregation bucket — once you cross 1000 distinct names, the SDK drops new ones with a warning.

**`pontem.init(stdlib_logging=True)` raises `RuntimeError`.** Root has no handlers to swap. Run `basicConfig` / `dictConfig` / `addHandler` *before* `init`. If you add handlers after `init`, call `PontemFormatter.install()` to apply the formatter to them.

