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 jsonfile: a rotating file at--log-file <path>journald: the systemd journal on Linuxsyslog: 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.logAvailability 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:
| Field | Meaning |
|---|---|
operation.traceparent | W3C trace context opened for the write |
operation.source | Node that accepted the write |
operation.authority | Authority the write mutates |
operation.epoch | Authority epoch the write committed under |
operation.serial | Journal serial from the write's own commit receipt, absent when the mutation journaled nothing |
operation.kind | Mutation class |
ack.policy | Configured durability policy |
ack.outcome | durable, pending, or unknown |
ack.scope | Durability scope proven, or none |
ack.evidence | filesystem for counted node receipts, object-store otherwise |
ack.nodes | Datacenter members whose receipt was counted |
ack.required | Byte copies the policy required |
ack.remaining | Byte copies still outstanding |
ack.bytes_acknowledged | Whether the byte dimension proved |
ack.metadata_acknowledged | Whether the metadata dimension reached the write's serial |
ack.bytes_expired | Whether the byte dimension's budget ran out |
ack.metadata_expired | Whether the metadata dimension's budget ran out |
ack.bytes_retired | Peers the write stopped asking, as node=reason pairs |
ack.metadata_retired | Datacenters the write stopped asking, as datacenter=reason pairs |
ack.waited_seconds | Time 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.