Documentation

Configure logging

Set the level with --log-level {error,warn,info,debug,trace} or [log] level. -v selects debug and -vv selects trace:

peryx serve --log-level debug

At the default info level, request records include the HTTP method, path, status, and latency. Ecosystem guides describe their client request sequences.

Sinks

--log-sink or [log] sink selects one destination:

  • stdout: terminal text, or JSON Lines with --log-format json
  • file: a rotating file at --log-file <path>
  • journald: the systemd journal on Linux
  • syslog: the local syslog daemon on Unix
[log]
level = "info"
format = "json"
sink = "file"
file = "/var/log/peryx/peryx.log"

Startup rejects invalid combinations, including a file sink without a path.

Security events

Repository actions emit structured records on the peryx::security target. JSON output supports filtering by actor, action, target, or result.

peryx serve --log-format json --log-sink file --log-file /var/log/peryx/events.log

Each repository-action record sets security_event=true and event=index_action. Shared fields include action, result, actor, presented_user, index, request_id, user_agent, and client_ip. Ecosystem owners may add subject identifiers. Missing string and numeric values use empty strings and zero. Records exclude credentials, bearer tokens, Basic passwords, and URL secrets.

actor names the identity authentication established, and is the only field to attribute an action to. For an index credential it is the matched token's name; for trusted publishing it is trusted-publisher:{binding}; for a minted scoped token it is the subject peryx signed into it. A request that authenticated as nobody has an empty actor, and so does a background action.

presented_user is the username the client sent, which nothing verifies. Basic credentials authenticate on the password alone, so this names no identity even beside a full actor: a twine upload always presents __token__, and a client is free to present a name that belongs to somebody else. It is kept so a failed attempt stays traceable. The field holds at most 64 characters and drops control characters, because the client chooses its contents.

client_ip records the request's transport peer. When the peer matches a rate_limit.trusted_proxies network, the field uses the client address from X-Forwarded-For or X-Real-IP. The rate limiter and security logger share this trusted-proxy decision. An untrusted peer cannot override the field with a forwarding header. Background actions and accepted requests without an attributable address use an empty string.

Server-role checks use event=authorization. Allowed records include user, scope, resource_kind, resource, result, and reason. Denied records omit the resource fields and use reason=no_grant or reason=storage_unavailable. They also omit the rejected URL and query parameters.

grep '"security_event":true' /var/log/peryx/events.log
jq 'select(.fields.security_event == true and .fields.result == "denied")' /var/log/peryx/events.log

Availability trace context

When distributed availability is configured, every committed write opens a W3C trace where its acknowledgement resolves, and records what the configured [availability.write_ack] policy proved. The trace and span identifiers are drawn from the operating system's entropy, so an identifier never repeats and the sampled flag is always set: a mutation is rare next to a read, and its record only exists to answer a question about one.

A blob write emits one availability blob write acknowledged event:

FieldMeaning
operation.traceparentW3C trace context opened for the write
operation.sourceNode that accepted the write
operation.authorityAuthority the write mutates
operation.epochAuthority epoch the write committed under
operation.serialJournal serial from the write's own commit receipt, absent when the mutation journaled nothing
operation.kindMutation class
ack.policyConfigured durability policy
ack.outcomedurable, pending, or unknown
ack.scopeDurability scope proven, or none
ack.evidencefilesystem for counted node receipts, object-store otherwise
ack.nodesDatacenter members whose receipt was counted
ack.requiredByte copies the policy required
ack.remainingByte copies still outstanding
ack.bytes_acknowledgedWhether the byte dimension proved
ack.metadata_acknowledgedWhether the metadata dimension reached the write's serial
ack.bytes_expiredWhether the byte dimension's budget ran out
ack.metadata_expiredWhether the metadata dimension's budget ran out
ack.bytes_retiredPeers the write stopped asking, as node=reason pairs
ack.metadata_retiredDatacenters the write stopped asking, as datacenter=reason pairs
ack.waited_secondsTime the acknowledgement spent resolving

A blob write is datacenter-durable only once both dimensions are, so both are reported: the outcome alone does not say which one a stalled write is waiting on. A metadata-only write, such as an OCI manifest, emits availability metadata write acknowledged with the same operation fields, ack.evidence=journal-frontier, and a single ack.expired and ack.retired because the journal frontier is its whole proof.

A dimension can also stop short without its budget running out. Some failures no later poll can revise: a rejected replication credential, a reply that is not a valid frontier or receipt, a receipt naming a different node. A source that fails that way is retired for the rest of the write, the write stops asking it, and the retired fields name it with a failure class such as unauthenticated or malformed. The class is a fixed token, never response body or credential text. Read them together with the expiry fields: an unproven write whose expiry fields are false was stopped by the sources named in the retired fields, not by its deadline.

A source can also be retired for retry_exhausted, which is the one class a later poll could have revised. A source failing retryably, such as one answering 503, is re-asked on widening backoff rather than on the poll cadence, and a source that spends its attempt limit inside one write is retired for the rest of that write. It starts the next write with a clean slate, so retry_exhausted reports a source that was failing throughout one write rather than one that has been taken out of service.

Both events carry identity and verdict only. They exclude payload bytes, metadata mutations, content references, credentials, and private paths.

Find every write that missed its durability level, and the members that did not answer:

jq 'select(.fields.message == "availability blob write acknowledged" and .fields."ack.outcome" != "durable")' \
  /var/log/peryx/events.log

A node that cannot read its own metadata position answers the frontier endpoint 500 rather than 404, so peers retry it as a fault instead of recording it as a node that has not applied the authority.

A replicated write also carries trace context in its operation envelope, so the producer, follower apply, and content copy join one trace. A replay retains the trace ID and operation identity but creates a new span ID for the apply work. A received envelope keeps the sampling its author chose, and an operation without the sampled flag emits no availability operation event.

On this page