StatOSS

Send logs

Every way in takes an app's key, and the lines land in that app.

The key

An app's key is the 32 characters of its DSN before the @, under the app's Keys. Send it as Authorization: Bearer <key>, as X-Statoss-Key: <key>, or as the password of HTTP Basic. The SDKs read it from the DSN. Each app's Logs tab has these snippets with its key filled in.

Lines go to https://ingest.statoss.com. What a team may keep is under Limits and the daily cap.

JavaScript and TypeScript: @statoss/sdk

Version 0.4.0 or later.

npm install @statoss/sdk

import * as StatOSS from "@statoss/sdk";

StatOSS.init({ dsn: "https://<key>@ingest.statoss.com/<app number>", logs: true });

StatOSS.logger.info("Order paid", { orderId: 7 });
StatOSS.logger.warn(StatOSS.logger.fmt`Search took ${ms} ms`);

Python: statoss

Version 0.2.0 or later.

pip install statoss

import statoss

statoss.init(dsn="https://<key>@ingest.statoss.com/<app number>", enable_logs=True)

statoss.logger.info("Order {order_id} paid", order_id=7)

Rust: statoss

Version 0.1.0 or later. The feature log takes the log crate.

cargo add statoss --features tracing
cargo add tracing tracing-subscriber

use tracing_subscriber::prelude::*;

let _statoss = statoss::init(statoss::Options::new().dsn("https://<key>@ingest.statoss.com/<app number>").logs(true));
tracing_subscriber::registry().with(statoss::tracing::layer()).init();

tracing::info!(order_id = 7, "Order paid");

Sentry's JavaScript SDKs

Version 9.41.0 or later. Before 10.71.0, add enableLogs: true.

import * as Sentry from "@sentry/node";

Sentry.init({ dsn: "https://<key>@ingest.statoss.com/<app number>" });

Sentry.logger.info("Order paid", { orderId: 7 });

Sentry's Python SDK

sentry-sdk 2.35.0 or later. Before 2.68.0, add enable_logs=True.

import sentry_sdk

sentry_sdk.init(dsn="https://<key>@ingest.statoss.com/<app number>")

sentry_sdk.logger.info("Order {order_id} paid", order_id=7)

OpenTelemetry SDKs

Python also needs opentelemetry-exporter-otlp-proto-http.

OTEL_LOGS_EXPORTER=otlp
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://ingest.statoss.com/v1/logs
OTEL_EXPORTER_OTLP_LOGS_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_LOGS_HEADERS=Authorization=Bearer%20<key>

OpenTelemetry Collector

Before Collector 0.144.0 the exporter is named otlphttp.

exporters:
  otlp_http/statoss:
    endpoint: https://ingest.statoss.com
    headers:
      Authorization: "Bearer <key>"

service:
  pipelines:
    logs:
      receivers: [otlp]
      exporters: [otlp_http/statoss]

Vercel

Pro and Enterprise. Team Settings, Drains, Add Drain: Logs, Custom Endpoint.

Endpoint  https://ingest.statoss.com/logs
Format    NDJSON
Header    Authorization: Bearer <key>

Fly.io

Fly's log shipper, as an app of its own.

fly launch --no-deploy --image ghcr.io/superfly/fly-log-shipper:latest
fly secrets set ORG=<org> ACCESS_TOKEN="$(fly tokens create readonly <org>)"
fly secrets set HTTP_URL=https://ingest.statoss.com/logs HTTP_TOKEN=<key>
# Delete [http_service] from fly.toml, then:
fly deploy --no-public-ips

Cloudflare Workers

Observability, Destinations, Add destination: type Logs.

Destination

OTLP endpoint  https://ingest.statoss.com/v1/logs
Header         Authorization: Bearer <key>

wrangler.jsonc

"observability": {
  "logs": {
    "enabled": true,
    "destinations": ["statoss"]
  }
}

Heroku

Cedar apps.

heroku drains:add https://statoss:<key>@ingest.statoss.com/logs/heroku -a <app>

JSON over HTTPS

One object, a JSON array, or NDJSON.

curl https://ingest.statoss.com/logs \
  -H "Authorization: Bearer <key>" \
  -H "Content-Type: application/json" \
  -d '{"level": "error", "message": "Card declined", "service": "checkout", "orderId": 7}'

JSON fields

A line sent as JSON, to /logs, is read like this. The drains' own fields, such as Vercel's projectName and Fly's app name, become the service.

ColumnFrom
Timetimestamp, time, ts, @timestamp or date: seconds, milliseconds, nanoseconds or ISO; else when it arrived
Levellevel, severity, log.level, lvl or levelname: a word such as warn or error, a pino or syslog number; else info
Bodymessage, msg, body, log or text; one that is not a string is kept as JSON
Serviceservice.name or service
Environmentdeployment.environment.name, deployment.environment, environment or env
Releaseservice.version or release
TracetraceId, trace.id or trace_id, and the span's likewise
Attributeseverything else, nested fields flattened with dots

Answers

StatusMeans
200Read. /logs answers {"accepted", "rejected"}; lines past the first 5,000 are rejected.
400Nothing in it could be read.
401No key.
403Not a live key, or logs are off for the app's team.
413Over 8 MiB, before or after decompression.
415A Content-Type the path does not take.
429Over the app's 60,000 lines a minute, or the team's daily cap; with Retry-After.
503Busy; with Retry-After.

OpenTelemetry senders get OTLP's own answers, with partial_success when some lines were dropped.