Metadata-Version: 2.4
Name: statoss
Version: 0.2.0
Summary: Error tracking and logs for StatOSS: sentry-sdk with your app's own levels, categories and fields.
Keywords: statoss,errors,error-tracking,logs,sentry,monitoring
Author: StatOSS
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: System :: Monitoring
Classifier: Typing :: Typed
Requires-Dist: sentry-sdk>=2.35.0,<3
Requires-Dist: typing-extensions>=4.12
Requires-Python: >=3.9
Project-URL: Homepage, https://statoss.com
Project-URL: Source, https://github.com/kroqdotdev/statoss/tree/main/sdk-python
Description-Content-Type: text/markdown

# statoss

Error tracking and logs for [StatOSS](https://statoss.com) in Python: a
thin layer over [`sentry-sdk`](https://pypi.org/project/sentry-sdk/) that
adds your app's own levels, categories and fields. Python 3.9 or later,
sentry-sdk 2.35 or later.

## Install

```sh
pip install statoss    # or: uv add statoss, poetry add statoss
```

## Start

Call `init` once, as early as you can, with your app's DSN. The DSN is on the
app's setup page in StatOSS and looks like
`https://<key>@ingest.statoss.com/<app number>`.

```python
import os
import statoss

statoss.init(
    dsn=os.environ["STATOSS_DSN"],
    environment="production",
    release="shop@1.4.0",
)
```

The options are sentry-sdk's, and without a schema `statoss.init` is
`sentry_sdk.init`: integrations, scopes and everything else work as its
documentation says. `statoss.flush`, `set_user`, `set_tag`, `set_context` and
`add_breadcrumb` are sentry-sdk's own; import `sentry_sdk` for the rest.

## Levels, categories and fields

Sentry has five levels: `fatal`, `error`, `warning`, `info` and `debug`.
StatOSS lets you name your own, and adds a category and fields.

```python
schema = statoss.define_schema(
    levels={
        "payment-failed": statoss.Level(like="error", label="Payment failed"),
        "slow-query": "warning",
    },
    categories={
        "billing": statoss.Category(label="Billing", color="plum"),
        "auth": statoss.Category(),
    },
    fields={
        "route": statoss.Field(label="Route", filterable=True),
        "duration_ms": statoss.Field(type="number", unit="ms", label="Duration"),
    },
)

statoss.init(dsn=os.environ["STATOSS_DSN"], schema=schema)

try:
    charge(order)
except CardError as error:
    statoss.capture_exception(
        error, level="payment-failed", category="billing", fields={"route": "/pay"}
    )
    # level "error", tags statoss.level=payment-failed, statoss.category=billing, route=/pay

statoss.capture_message("Search took 2.4 s", level="slow-query", fields={"duration_ms": 2400})
# level "warning", tags statoss.level=slow-query, duration_ms=2400

statoss.set_category("billing")  # every later event in this scope
```

- An event is sent at the nearest Sentry level, with its own level in the tag
  `statoss.level` and its category in `statoss.category`, so Sentry and other
  Sentry-compatible tools still read it. Fields are sent as tags.
- `level` takes a Sentry level or a level of the schema. A name the schema
  does not hold is sent as `error` and, with `debug=True`, logged. A level or
  category with characters a name cannot have (see below) is left out and
  logged the same way.
- The level given to a capture wins over a level set on a scope.
- `set_category(name, scope=None)` sets it on the scope you pass, else on the
  isolation scope as `sentry_sdk.set_tag` does. `set_category(None)` takes it
  off.
- An event whose level is one of the schema's own, as `scope.set_level` or
  `sentry_sdk.capture_message` may leave it, is sent the same way, after your
  `before_send`.

## Logs

With `enable_logs=True`, `statoss.logger` sends log lines through
sentry-sdk's logger, which StatOSS's Logs area searches. Without it they
are dropped.

```python
statoss.init(dsn=os.environ["STATOSS_DSN"], schema=schema, enable_logs=True)

statoss.logger.info("Order {order_id} paid", order_id=7, attributes={"amount": 12.5})
statoss.logger.log("payment-failed", "Card declined", attributes={"order_id": 7})
# level "error", attributes statoss.level=payment-failed, order_id=7
```

- `trace`, `debug`, `info`, `warning`, `error` and `fatal` take a template
  and its values as sentry-sdk's do: `"Paid {amount}"` with `amount=12`
  sends "Paid 12", the template and each value as attributes.
  `attributes=` adds more.
- `log(level, template, ...)` takes a level by name: one of the six (`warn`
  too), a Sentry level, or one of the schema's. A schema level is sent at
  its range's level (`trace` stays `trace`), with its severity as
  `sentry.severity_number` when the schema gives one, and its name as the
  attribute `statoss.level`. A name the schema does not hold is sent as
  `error` and, with `debug=True`, logged.
- `client.logger` from `init` takes only the schema's levels when they are
  typed (see [Typing](#typing)).
- Lines are sent in batches by sentry-sdk; `statoss.flush()` sends what
  waits. sentry-sdk's `before_send_log` sees each line, `statoss.level`
  included.
- Records of Python's `logging` module at INFO and up go as log lines too
  with `enable_logs=True`, except on sentry-sdk 2.68.0, where they need
  `LoggingIntegration(capture_sentry_logs=True)`.

## The schema

`define_schema` checks everything as StatOSS does and raises `SchemaError`
naming the place of the first problem.

| Kind     | Name                                                           | Settings                                                                                                                          |
| -------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Level    | 1 to 32 lowercase letters, digits, dots, dashes or underscores | `like` (`fatal`, `error`, `warning`, `info`, `debug` or `trace`), `label`, `color`, `severity` (1 to 24, inside the `like` range) |
| Category | 1 to 64 of the same                                            | `label`, `color`, `hidden`, `owner` (a team member's email)                                                                       |
| Field    | any tag key not starting `statoss.`                            | `label`, `type` (`text` or `number`), `unit`, `filterable`                                                                        |

- A level may be given as its `like` alone (`"slow-query": "warning"`);
  categories and fields as a list of names (`categories=["billing", "auth"]`).
- A level ranking like `trace` is sent as `debug`. Sentry's own levels take a
  label and a colour (`"error": statoss.Level(label="Error")`).
- Colours: crimson, red, orange, amber, yellow, green, aqua, teal, blue,
  violet, indigo, magenta, plum, grey.
- Rules sort events that name no level or category of their own:

  ```python
  rules = [
      statoss.Rule(subject="logger", value="payments", category="billing"),
      statoss.Rule(subject="tag", tag_key="db", match="starts", value="pg", level="slow-query"),
  ]
  ```

  `subject` is `tag`, `logger`, `exception`, `culprit`, `title` or
  `message`; `match` is `is` (the default), `starts` or `contains`.

`schema.to_desired()` gives the schema as StatOSS's desired state, the JSON
that `POST /api/v1/apps/{app}/schema/plan` takes as `desired`. Rules left out
(`rules=None`) leave the app's rules as they are; `rules=[]` says there are
none.

## Typing

The package ships type hints. Name your levels and categories with `Literal`
and a type checker (mypy, pyright) refuses a name the schema does not hold:

```python
from typing import Literal

import statoss

LevelName = Literal["payment-failed", "slow-query"]
CategoryName = Literal["billing", "auth"]

schema: statoss.Schema[LevelName, CategoryName] = statoss.define_schema(
    levels={"payment-failed": "error", "slow-query": "warning"},
    categories=["billing", "auth"],
)
client = statoss.init(dsn=os.environ["STATOSS_DSN"], schema=schema)

client.capture_exception(error, level="payment-failed", category="billing")
client.capture_exception(error, level="payment-faild")  # error: not a LevelName
client.logger.log("slow-query", "Search took {ms} ms", ms=2400)
client.logger.log("slow-qeury", "Search took {ms} ms", ms=2400)  # error: not a LevelName
```

`init` returns a `statoss.Client[LevelName, CategoryName]` whose
`capture_exception`, `capture_message`, `set_category` and `logger.log`
take only those names and Sentry's levels. The module functions and
`statoss.logger` take any name.

## Developing

From `sdk-python/`, with [uv](https://docs.astral.sh/uv/):

```sh
uv run pytest                      # tests, including the mypy check of tests/typing_check.py
uv run --with pyright pytest       # the same, with pyright as well
uv build                           # wheel and sdist in dist/
```

`tests/fixtures` holds a schema's desired state and the lists and limits the
package checks against; `src/lib/python-sdk-schema.test.ts` holds the server
to them. After changing either side, run `uv run python tests/sample_schema.py`
and `pnpm prettier --write sdk-python/tests/fixtures`.

## License

MIT
