Metadata-Version: 2.4
Name: pontem
Version: 0.2.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

Pontem Python SDK for logs, metrics, and config on Pontem-managed edge devices. Zero runtime dependencies.

## Installation

```bash
pip install pontem
```

## Quick Start

**Already using stdlib `logging`?** This is the fastest integration — one extra `init` flag, no call-site changes:

```python
import logging
import pontem

# 1. Your existing logging config runs first — unchanged.
logging.basicConfig(level=logging.INFO)

# 2. One init call enables the integration.
pontem.init(service_name="my-service", stdlib_logging=True)

# 3. Existing call sites keep working, now emit Pontem JSONL on the wire.
logging.getLogger(__name__).info("model loaded", extra={"model": "v3"})
```

`stdlib_logging=True` swaps the formatter on every handler currently attached to the root logger. Destinations, rotation policies, and filters stay in place — only the on-the-wire format changes. `pontem.logger.*` continues to work alongside; records from either path share the same wire shape. SDK-internal records (`pontem.sdk`, `pontem.emit`, …) propagate to your chain too — they include useful WARN/ERROR signal (queue overflow, writer errors). Filter the noise via `logging.getLogger("pontem").setLevel(logging.WARNING)` if you want only the loud ones.

**Order matters:** run your logging setup (`basicConfig`, `dictConfig`, manual `addHandler` calls, …) *before* `pontem.init(stdlib_logging=True)`. Otherwise root has no handlers to swap and init raises. See [Logging](#logging) for explicit-handler and `dictConfig` patterns where this ordering is structural rather than implicit.

**Greenfield code, or a hot path?** Call `pontem.logger.*` directly. The direct API is non-blocking, queued, with background-thread I/O — designed for inference loops on edge devices:

```python
import pontem

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

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

pontem.metrics.count("detections", class_name="apple")
with pontem.metrics.timer("model.inference"):
    result = model.predict(frame)

pontem.shutdown()
```

## Logging

Structured logs with OTel-aligned severity levels (`severityNumber` `1`/`5`/`9`/`13`/`17`/`21` → `TRACE`/`DEBUG`/`INFO`/`WARN`/`ERROR`/`FATAL`). The wire format is identical regardless of which path you use — collectors see one shape. See [`SCHEMA.md`](./SCHEMA.md) for the full record contract.

### Drop-in via `pontem.init(stdlib_logging=True)` (recommended)

The one-flag path is the easiest on-ramp:

```python
import logging
import pontem

logging.basicConfig(level=logging.INFO)
pontem.init(service_name="my-service", stdlib_logging=True)

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

What the flag does:

1. Installs `PontemFormatter` on every handler currently on the root logger. Records flow through your existing handler chain unchanged, just with a Pontem-shaped wire format.
2. That's it. No other side effects. SDK-internal records (`pontem.sdk`, `pontem.emit`, …) propagate to your chain like any other library's logs — useful for debugging integration issues (queue overflow, writer errors). If you don't want them, filter via standard stdlib mechanisms: `logging.getLogger("pontem").setLevel(logging.WARNING)` to keep only WARN+, or `.propagate = False` to drop them entirely.

`pontem.logger.*` continues to work alongside — it goes through the SDK's emit pipeline and produces the same wire shape. You can mix paths if you want (e.g., direct API on a hot inference path, stdlib elsewhere); the collector sees one schema.

### Explicit `PontemFormatter` (advanced)

When you want to attach the formatter to specific handlers — e.g. a `RotatingFileHandler` writing to a custom path — use `PontemFormatter` directly. The flag-based path won't help you here because it only touches root handlers and you may not want all of them reformatted.

```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)
```

For `logging.config.dictConfig` (YAML/JSON config), reference the formatter by class:

```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`, `device.id`, …) come from constructor kwargs first, then fall back to whatever `pontem.init()` populated. `service.name` is required from at least one source — the constructor raises if neither provides it.

Notes (apply to both paths):

- **Set the root level.** Stdlib's default is `WARNING`, so `info`/`debug` records are filtered before reaching any handler. `basicConfig(level=logging.INFO)` or `logging.getLogger().setLevel(...)` is the standard fix.
- **Records do not flow through Pontem's emit pipeline.** They take whatever path your existing handler already uses (sync I/O for `FileHandler`, etc.). For non-blocking, queued, rotation-and-gzip behavior, use the direct API on hot paths.

### Direct API

For greenfield code or hot paths where you want the SDK's non-blocking emit pipeline (bounded queue, background-thread serialization, rotation + gzip in file mode):

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

Available levels: `trace`, `debug`, `info`, `warn`, `error`, `fatal`.

In file mode (default), each log is written to `logs.jsonl` in the emit directory. In stdout mode, records are written one-per-line to `sys.stdout`. See [Stdout emit](#stdout-emit-docker--compose) below.

## Stdout emit (Docker / compose)

This setting only affects the direct API. The Formatter path always writes through the user's own handler chain and is unaffected.

For containerized deployments where a sidecar log collector tails the container's stdout, switch the direct API from file mode to stdout mode:

```python
import pontem

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

Equivalently, set the env var (useful when the same image runs on both edge devices and compose hosts):

```bash
PONTEM_EMIT_TARGET=stdout
```

Selection precedence: `init(emit_target=...)` kwarg > `PONTEM_EMIT_TARGET` env var > 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.

## Metrics

Metrics are aggregated in memory and flushed periodically to `metrics.jsonl`. No I/O on the caller's thread.

```python
# Counters — incremented, flushed as a single sum
pontem.metrics.count("detections", class_name="apple")
pontem.metrics.count("bytes_sent", len(payload))

# Histograms — records min/max/sum/count summary
pontem.metrics.record("payload_size", len(data), unit="bytes")

# Gauges — last value wins
pontem.metrics.set_gauge("gpu_temp", 72.0, unit="celsius", gpu="0")

# Timers — context manager or decorator, records to histogram
with pontem.metrics.timer("model.inference"):
    result = model.predict(frame)

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

## Config

Reads agent-managed config from `/opt/pontem/config/config.json` (or `$PONTEM_CONFIG_DIR`).

```python
threshold = pontem.config("my_namespace", "model_threshold", default=0.85)

# Reload after agent signals a config update
pontem.config.reload()
```

## Shutdown

Flushes remaining metrics and logs. Also registered via `atexit`.

```python
pontem.shutdown()
```

## Documentation

See [docs/getting-started.md](docs/getting-started.md) for a comprehensive guide including full API reference, configuration options, and troubleshooting.

## License

Apache 2.0 — see [LICENSE](LICENSE).

