Metadata-Version: 2.4
Name: statoss
Version: 0.1.0
Summary: Error tracking for StatOSS: sentry-sdk with your app's own levels, categories and fields.
Keywords: statoss,errors,error-tracking,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.0.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 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.

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

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

`init` returns a `statoss.Client[LevelName, CategoryName]` whose
`capture_exception`, `capture_message` and `set_category` take only those
names and Sentry's levels. The module functions 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
