Documentation menu

Structured application logs

Metrics → Logs searches named operational events by severity, service, release, event ID, static message and trace ID. Application logs have their own retention and deletion controls. The append-only security audit ledger is separate.

This is bounded structured event logging, not a general-purpose text log archive or a Loki-compatible query service. Arbitrary log bodies and string attributes are intentionally not retained.

Approve the events first

Open Metrics → Logs → Setup and policy in the intended environment. Add stable event IDs with operator-reviewed static messages, for example:

Event ID Static message Numeric or boolean fields
application.started Application started worker_count
job.completed Job completed duration_ms, retry_count, cache_hit
job.failed Job failed duration_ms, retry_count

Messages never interpolate payload values. Review them as non-sensitive operational text. Credentials, URLs, emails, IPs and placeholders are refused in templates. Unknown event IDs are dropped and counted; a new environment has no approved events until you save its policy.

Only configured finite numeric and boolean attributes survive. Strings, nested objects, arrays, headers, bodies, SQL, exception messages, personal identifiers and sensitive attribute keys are discarded before storage. Service and release are bounded technical labels scrubbed using the trace collector's redaction rules; use non-sensitive service names and release versions. No raw body, rejected event name or original attribute value is written into audit metadata.

Changing a message affects new records. Removing an event immediately hides its history and lets maintenance remove the payloads. Removing a numeric field hides it from reads immediately. History may contain the older approved static message until it expires or is deleted.

Node.js

Use the optional backend entry point. Approve the example event and its fields first.

import { createLogBatch } from "@saaspro/node/logs";

const batch = createLogBatch({
  key: process.env.SPM_SECRET_KEY!,
  host: process.env.SPM_HOST,
  service: "worker",
  release: "1.2.3"
}, [{
  event: "job.completed",
  severity: "info",
  attributes: { duration_ms: 42, retry_count: 0, cache_hit: true },
  trace: { traceId: "11111111111111111111111111111111", spanId: "2222222222222222" }
}]);

const receipt = await batch.send();
// Inspect accepted, rejected and retryable.
// Retry the SAME batch only if retryable is true, using bounded backoff.

Omit trace context when none exists; the IDs above illustrate shape only. Pass capture-time OpenTelemetry span context when correlating a real request. Batches contain 1–100 records and at most 64 KiB; each gets a stable UUID and timestamp at preparation. Concurrent sends of one batch share its in-flight request. Partial success and terminal refusals are not sent again. The helper has a five-second transport timeout, bounded receipts, HTTPS outside loopback and no redirects or background queue.

Python

import os
from saaspro import SaaSProMax
from saaspro.logs import LogBatch

with SaaSProMax(os.environ["SPM_SECRET_KEY"], host=os.environ["SPM_HOST"]) as client:
    batch = LogBatch(client, [{
        "event": "job.completed",
        "severity": "info",
        "attributes": {"duration_ms": 42, "cache_hit": True}
    }], service="worker", release="1.2.3")
    receipt = batch.send()
    # Retry the same batch with bounded backoff only when receipt["retryable"].

trace_id, span_id, UUID id and integer timestamp_ns are optional entry fields. String attributes and unsupported fields are refused before transport. The helper preserves an immutable payload, bounds its transport deadline to five seconds and refuses batches inherited across a process fork. Use a new client and batch in a child process.

OpenTelemetry and Collector

The endpoint is POST /api/v1/otlp/v1/logs, authenticated with a secret ingest key from the destination application and environment. Public ingest keys, Management API tokens and requests with a browser Origin are refused. No browser CORS or gRPC endpoint is exposed.

Standard OTLP HTTP exporters can send JSON or Protobuf, optionally gzip. Configure your exporter with the complete log endpoint:

export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT="$SPM_HOST/api/v1/otlp/v1/logs"
export OTEL_EXPORTER_OTLP_LOGS_HEADERS="Authorization=Bearer $SPM_SECRET_KEY"
export OTEL_EXPORTER_OTLP_LOGS_PROTOCOL="http/protobuf"

A Collector can receive existing SDK/gRPC sources and forward logs over HTTP. The following bounded gateway pipeline forwards records to the server, which applies approved-event and field checks before storage. Supply credentials through the Collector environment, never browser code.

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 127.0.0.1:4317
      http:
        endpoint: 127.0.0.1:4318
processors:
  memory_limiter:
    check_interval: 1s
    limit_mib: 128
  batch:
    send_batch_size: 100
    send_batch_max_size: 100
exporters:
  otlphttp/saaspro:
    logs_endpoint: ${env:SPM_HOST}/api/v1/otlp/v1/logs
    headers:
      Authorization: Bearer ${env:SPM_SECRET_KEY}
    compression: gzip
service:
  pipelines:
    logs:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlphttp/saaspro]

This pipeline forwards source records; add the Collector's appropriate redaction/transform processor if sensitive payloads must not leave the source network. Server redaction occurs before database storage. Set eventName on current SDK log records, or the event.name attribute on older integrations. Only approved IDs are retained. service.name and service.version resource attributes become service and release filters. The original body, severity text, instrumentation scope, resource extras and unapproved attributes are discarded.

Protocol fields and severity mapping follow the OpenTelemetry log data model and OTLP HTTP specification. Severity numbers 1–4 are trace, 5–8 debug, 9–12 info, 13–16 warn, 17–20 error and 21–24 fatal; zero is unspecified. A valid event timestamp is required, using observedTimeUnixNano if timeUnixNano is absent or zero. Records outside effective retention or over one minute in the future are rejected. Invalid trace context is discarded independently; a span link requires a valid trace ID.

Receipts, retries and limits

HTTP 200 {} confirms full acceptance, including normalized duplicates. Partial acceptance returns partialSuccess.rejectedLogRecords and a bounded diagnostic with counts of invalid/deleted/conflicting, unapproved and over-budget records. Do not retry partial success. The receipt confirms storage policy handling, not application health or completeness.

Prefer a UUID log.record.uid attribute for immutable record identity. Exact retries with the same UID and normalized content do not consume storage quota. Changed content under the same UID is rejected. Without a UID, normalized event timestamp, event, severity, service, release, trace/span and numeric fields form a fingerprint; indistinguishable records at the exact same nanosecond collapse into one. Raw discarded content is never fingerprinted.

Limits per environment: 200,000 physical records and 128 MiB normalized payload, with 100,000 new records per UTC day by default (configurable from 100 to 1,000,000). Exports contain at most 512 records, 2 MiB expanded or 1 MiB compressed; each normalized record is at most 8 KiB. Requests have a per-key, per-process bucket of 20 exports with two exports replenished per second. Collection and policy/quota/deletion changes serialize under an environment lock; storage statements are limited to eight seconds.

HTTP 400/403/413/415 indicate invalid input, capability/policy refusal or limits. 401 means missing, unknown or revoked credentials. 429 and 502/503/504 permit the same immutable export to be retried with bounded backoff. Never log credentials or original response data as diagnostic text.

Retention defaults to seven days, configurable from one to thirty and capped by application retention. Reads enforce it immediately. Every five minutes, maintenance visits at most fifty environments in oldest-maintained order and removes at most 10,000 expired, deleted-window or removed-event records per environment. The same pass removes at most 10,000 expired deletion markers and 1,000 old receipt-day rows. Aggregate receipt counters retain ninety UTC days; they contain no log payloads. Physical quotas include records awaiting removal.

Deleting one record blocks its record identity for thirty days. Deleting all collected logs records a cutoff at the later of the current time and the latest accepted event time, hides the entire earlier window immediately and prevents old exports from restoring it. Future-dated records can therefore extend the cutoff by at most one minute. Daily admission totals do not reset. These actions require Metrics write, a reason and a fresh passkey. They never modify or delete security audit events.

Private reads require Metrics read and tenant membership. Management clients may GET /api/v1/apps/{app}/metrics/logs?env=production, with optional severity, service, release, event, traceId, q, range, cursor and limit (1–100). Add id for one retained record. Trace links resolve only inside the same application/environment and trace retention. A missing trace may be delayed, sampled, deleted or expired. Collection-policy and deletion writes remain in the authenticated console.