Metadata-Version: 2.4
Name: pontem
Version: 0.2.1
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. Telemetry lands as JSONL under `/opt/pontem/<service_name>/var/log/`. The agent picks it up.

> **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`, `device.id`, …) 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 flushes summaries to `metrics.jsonl` periodically. All public methods return in under 1µs.

### Counter

Incremented each call; flushed as a single sum per `(name, attrs)`.

```python
pontem.metrics.count("detections", class_name="apple")
pontem.metrics.count("bytes_sent", len(payload))
```

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

### Histogram

Records `count`, `sum`, `min`, `max` per flush interval.

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

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

### Gauge

Last write wins per flush interval.

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

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

### 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", **attrs)
```

### Cardinality

Each unique `(name, attributes)` pair is its own aggregation bucket. Don't put unbounded values (request IDs, timestamps, user-supplied strings) in attributes — memory grows without bound.

---

## Config

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

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

Lookup key is `f"{namespace}:{key}"`. `default` is returned when the key is absent.

---

## 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):

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

Files rotate at 10 MB, are gzip-compressed on the background thread, and up to 5 rotated files are kept per channel.

---

## 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
    emit_target="file",          # "file" (default) or "stdout"
    stdlib_logging=False,        # True → install PontemFormatter on root handlers
)
```

`init()` kwargs take precedence over environment variables. Call once at startup.

### `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`       | JSONL output directory             | `/opt/pontem/<service_name>/var/log` |
| `PONTEM_EMIT_TARGET`    | `"file"` or `"stdout"`             | `"file"`                             |
| `PONTEM_CONFIG_DIR`     | Directory containing `config.json` | `/opt/pontem/config`                 |
| `PONTEM_DEVICE_ID`      | Device identifier (set by agent)   | —                                    |
| `PONTEM_TENANT_ID`      | Tenant identifier (set by agent)   | —                                    |
| `PONTEM_CONFIG_VERSION` | Config version (set by agent)      | —                                    |

### 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"}
}
```

Metric record:

```json
{
  "timestamp": "2025-01-15T10:30:01.000000Z",
  "name": "detections",
  "type": "counter",
  "value": 47,
  "attributes": {"class_name": "apple"},
  "resource": {"service.name": "my-service"}
}
```

The full schema lives in [SCHEMA.md](./SCHEMA.md).

---

## Troubleshooting

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

**`pontem.config(...)` always returns `default`.** Confirm the agent has written `config.json`: `cat /opt/pontem/config/config.json`. Verify `namespace` and `key` match what the agent provides (full key is `"{namespace}:{key}"`). 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 probably have unbounded attribute values (request IDs, timestamps) on a metric. Each unique `(name, attributes)` pair is a separate bucket — keep attribute values low-cardinality.

**`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.

