Documentation
¶
Overview ¶
Package logging gives Atlas's operational logs a stable contract (ADR-0142).
Every line Atlas wrote used to be a prose sentence with values interpolated into it:
log.Printf("checkpoint: published at log position %d (recovery replays only past it)", pos)
An operator can read that. Nothing can *alert* on it, except by matching a regular expression against wording that changes the moment someone rewords the sentence, and nothing can chart the position without parsing it back out of English.
The fix is not to delete the prose. "will retry next tick" is real guidance that a bare event name loses, and the console is a first-class audience — an operator watching `atlas serve` should not be handed JSON. So each line now carries three things:
- an **event name**, the stable identifier an alert matches on;
- the **sentence**, unchanged in spirit, as the human explanation;
- the **values**, moved out of the sentence into typed attributes.
Two properties are structural rather than conventional. Event is a struct with an unexported field, so a caller outside this package cannot invent a name — only the constants declared here exist, and a duplicate or malformed one panics at init rather than reaching production. And the standard logger ends up pointed at the same handler, so a line from a dependency arrives in one stream and one format instead of alongside it — see Setup for why that costs no code.
It is built on log/slog, which is to say on nothing: no dependency is added (ADR-0010), and the engine still does not log at all, so the single writer's hot path is untouched (invariants I1 and I3).
Index ¶
Constants ¶
const DefaultFormat = FormatText
DefaultFormat is text. The console audience is the one Atlas has always had, and a default that turns their terminal into JSON would be a regression dressed as progress; JSON is one flag away for the deployment that wants it.
Variables ¶
var ( // ServerListening is emitted once the HTTP listener is up. It comes *after* // recovery — the port stays closed until the log has been replayed (slice 7) — so // it doubles as the "this instance finished starting" signal. ServerListening = newEvent("server.listening") ServerShuttingDown = newEvent("server.shutting_down") ServerDocsEnabled = newEvent("server.docs_enabled") ServerMetrics = newEvent("server.metrics_enabled") // The operator-supplied certificate, where this server terminates TLS itself // (ADR-0191). ServerTLSReloaded is one line per renewal picked up without a // restart; ServerTLSReloadFailed is the pair that changed on disk and could not // be loaded, which leaves the previous certificate in service rather than // refusing handshakes — so it is a WARN an operator must act on before the // certificate that is still being served expires. ServerTLSReloaded = newEvent("server.tls_reloaded") ServerTLSReloadFailed = newEvent("server.tls_reload_failed") DataDirOpened = newEvent("server.data_dir_opened") // AuthDisabled is a server started with --auth=false: no login is required for // anything. It is a WARN and it is loud because it is now the deliberate // exception rather than the default — the one line that says this instance is // open to whoever can reach the port (ADR-0195). AuthDisabled = newEvent("auth.disabled") // The security audit trail (ADR-0197). Atlas's // business trails were always strong — every state transition, every variable // override, every task claim, each with its actor — but who signed in, who // failed to, and who changed an account or a credential was written down // nowhere. That is the first thing an audit asks for and the last thing these // events leave unanswered. // // Each carries the acting principal and the client address, and none of them // carries a secret: no password, no token, not even a truncated one. What is an // attribute here is what a log shipper extracts, indexes and keeps. AuthLogin = newEvent("auth.login") AuthLoginFailed = newEvent("auth.login_failed") AuthLoginThrottled = newEvent("auth.login_throttled") AuthLogout = newEvent("auth.logout") // AuthDenied is an authenticated caller refused for lacking a role — an // administration attempt by somebody who may not administer, which is a // different and much rarer signal than an anonymous request being asked to log // in. Only the authorization refusal is recorded; logging every 401 would bury // it under every unauthenticated probe on the internet. AuthDenied = newEvent("auth.denied") // The account lifecycle, and the credentials that are not accounts. AuthUserCreated = newEvent("auth.user_created") AuthUserUpdated = newEvent("auth.user_updated") AuthUserDeleted = newEvent("auth.user_deleted") AuthPasswordSet = newEvent("auth.password_set") AuthTokenMinted = newEvent("auth.token_minted") AuthTokenRevoked = newEvent("auth.token_revoked") // AuthOIDCConfigured is said once at startup where an operator named an identity // provider. It is the line that tells whoever reads the log after a failed login // which issuer this server was asking, which is the first thing to check. AuthOIDCConfigured = newEvent("auth.oidc_configured") // AuthOIDCMappingSet records a change to the mapping from a provider's claims // onto Atlas roles and groups. It is the change that explains every role change // after it: from the moment the mapping is on, whoever administers the provider's // groups administers this instance's roles. AuthOIDCMappingSet = newEvent("auth.oidc_mapping_set") // AuthRolesUpgraded is said once, on the first start after roles per endpoint // group shipped, naming how many accounts kept what they could already do. It is // the line an operator needs to see: nothing is narrower than it was yesterday // until somebody narrows it. AuthRolesUpgraded = newEvent("auth.roles_upgraded") // The OAuth authorization server (ADR-0200). Registering a client is an admin // act like minting a token; the rest is one person deciding, which is the event // an audit of "who let that application in" is looking for. AuthOAuthDenied // covers both halves of a refusal — the person declining, and the server // refusing a request that never should have been made — because for an operator // reading a log they are the same question: why did this not connect. AuthOAuthClientRegistered = newEvent("auth.oauth_client_registered") // AuthOAuthClientSelfRegistered is a client that registered *itself* (RFC 7591), // which only happens where an operator opened that. Kept apart from the admin // act above rather than folded into it: "an administrator added an application" // and "a stranger added one" are the same sentence with entirely different // consequences, and an audit that cannot tell them apart is not one. AuthOAuthClientSelfRegistered = newEvent("auth.oauth_client_self_registered") AuthOAuthClientDeleted = newEvent("auth.oauth_client_deleted") AuthOAuthGranted = newEvent("auth.oauth_granted") AuthOAuthDenied = newEvent("auth.oauth_denied") AuthOAuthTokenIssued = newEvent("auth.oauth_token_issued") AuthOAuthGrantRevoked = newEvent("auth.oauth_grant_revoked") // AuthOAuthRegistrationOpen is said once at startup where an operator opened // self-registration, the way AuthDisabled is said for a server without a login. // A setting that widens what an unauthenticated caller may do is one nobody // should be able to inherit without being told (ADR-0200). AuthOAuthRegistrationOpen = newEvent("auth.oauth_registration_open") // Worker ownership (ADR-0205). Who may reach a worker's configuration — // its endpoint, its credential reference and its inbound subscriptions — is now // somebody's decision rather than everybody's, so the decision is recorded. // Sharing and withdrawing are separate events because "who was let in" and "who // was shut out" are separate questions in an incident. // AuthWorkerTokenUnknown is an operator-set ATLAS_TOKEN that this server does // not accept. The supervisor honours the variable and stops injecting its own, // so the workers it starts would hold a credential refused at every poll — a // trap that used to be silent because no value could ever have worked // (ADR-0194). AuthWorkerTokenUnknown = newEvent("auth.worker_token_unknown") // CommandFailed is a top-level command exiting non-zero. CommandFailed = newEvent("command.failed") MCPProxying = newEvent("mcp.proxying") // WorkerStarting is the out-of-process job worker announcing what it will serve // and for which server (ADR-0157). WorkerStarting = newEvent("worker.starting") // WorkerPollFailed is a worker reporting that a poll failed and will be retried. WorkerPollFailed = newEvent("worker.poll_failed") // WorkerSupervisorStarted and WorkerSupervisorFailed report the lifecycle of a // worker process Atlas launched itself (ADR-0157 step 7). WorkerSupervisorStarted = newEvent("worker.supervised_started") WorkerSupervisorFailed = newEvent("worker.supervise_failed") // ADMockEnabled is an AD worker announcing that it serves the Active Directory // worker against a directory in its own memory rather than a real one // (ADR-0181). It is a warning rather than an info because // a mock worker is indistinguishable from a working one everywhere else: it // completes every job it leases. ADMockEnabled = newEvent("ad_mock.enabled") // ADMockPerformed is one operation that mock directory simulated. It is what a // mockup run leaves behind for the person who ran it, in the worker's log where // the Workers console shows it. ADMockPerformed = newEvent("ad_mock.performed") // ADMockReportFailed is a mock worker that could not deliver its directory to the // Atlas whose Console shows it (ADR-0213). A // warning, never a job failure: the operation it describes has already happened, // so what is lost is one refresh of a view that the next operation sends again. ADMockReportFailed = newEvent("ad_mock.report_failed") // ADMockSeedUnusable is a mock directory that could not read the seed it was // pointed at, and started empty instead. It is a warning and not a refusal // because a mock touches nothing real: an empty directory costs a joiner nothing // and costs a leaver one visible incident, whereas refusing takes the worker down // for every AD task at once — which is what an optional field with a typo in it // used to do (ADR-0202). ADMockSeedUnusable = newEvent("ad_mock.seed_unusable") // SQLMockEnabled is a SQL worker announcing that it answers database tasks from // seeded answers in its own memory rather than from a database (ADR-0221). // A warning for the same reason the AD one is: a mock worker is indistinguishable // from a working one everywhere else, because it completes the jobs it answers. SQLMockEnabled = newEvent("sql_mock.enabled") // SQLMockSeedUnusable is a mock database that could not read the seed file it was // pointed at, and started with no answers instead. A warning and not a refusal for // the reason ADMockSeedUnusable is one: the supervisor restarts a child that // exits, so failing on an optional file is a restart loop rather than a message. // Every statement then fails naming itself, which points at the missing seed. SQLMockSeedUnusable = newEvent("sql_mock.seed_unusable") // SQLMockReportFailed is a mock SQL worker that could not deliver its journal to // the Atlas whose Console shows it. A warning, never a job failure, for the reason // ADMockReportFailed is one: the statement it describes has already been answered // and the job has already settled, so what is lost is one refresh of a view that // the next statement sends again. SQLMockReportFailed = newEvent("sql_mock.report_failed") // WorkerHistoryFailed is the job-history exporter reporting that an append did not // reach its clio worker, or that its buffer is dropping entries. Both are // warnings rather than errors on purpose: the history is telemetry, and the engine // deliberately does not wait for it, so a gap costs a run nothing. WorkerHistoryFailed = newEvent("worker.history_failed") )
Process lifecycle.
var ( CheckpointEnabled = newEvent("checkpoint.enabled") CheckpointPublished = newEvent("checkpoint.published") CheckpointFailed = newEvent("checkpoint.failed") CheckpointPruneFailed = newEvent("checkpoint.prune_failed") WALCompactionEnabled = newEvent("wal_compaction.enabled") // WALCompactionInert is compaction configured *without* checkpointing, which does // nothing at all: the cut is derived from a checkpoint. It warrants a warning // precisely because the flag makes it look enabled. WALCompactionInert = newEvent("wal_compaction.inert") WALCompactionFailed = newEvent("wal_compaction.failed") WALCompactionWatermarkFailed = newEvent("wal_compaction.watermark_unavailable") WALCompactionSegmentsDeleted = newEvent("wal_compaction.segments_deleted") )
Recovery checkpoints and WAL compaction (ADR-0131).
var ( RestoreApplied = newEvent("restore.applied") BackupStreamFailed = newEvent("backup.stream_failed") FullBackupStreamFailed = newEvent("full_backup.stream_failed") ApplicationSourceStreamFailed = newEvent("application_source.stream_failed") )
Backup, restore, and streaming exports (ADR-0107/0108/0109).
var ( RetentionEnabled = newEvent("retention.enabled") RetentionPurged = newEvent("retention.purged") ExporterEnabled = newEvent("exporter.enabled") ExporterIndexed = newEvent("exporter.indexed") ExporterTickFailed = newEvent("exporter.tick_failed") )
History retention (ADR-0115/0144) and the OpenSearch exporter (ADR-0114).
var ( VaultKeyGenerated = newEvent("vault.key_generated") // JobTypeIndexCollision reports a stored job-type assignment whose index the // reserved range has since grown over. Warned at startup rather than swallowed: // jobs already on disk carry the old index, so the drop is not cosmetic. JobTypeIndexCollision = newEvent("jobtype.index_collision") AuthAdminSeeded = newEvent("auth.admin_seeded") AuthPasswordReset = newEvent("auth.password_reset") UserProvisioningUserCreated = newEvent("user_provisioning.user_created") UserProvisioningPasswordSet = newEvent("user_provisioning.password_set") UserProvisioningUserDisabled = newEvent("user_provisioning.user_disabled") )
Identity, secrets, and provisioning (ADR-0044/0070/0123).
var ( TracingEnabled = newEvent("tracing.enabled") TracingShutdownFailed = newEvent("tracing.shutdown_failed") )
Distributed traces (ADR-0142 slice 8b).
var ( // DeploymentReloadedWithProblems reports a stored definition — or a DMN model // bundled with one, told apart by the artifact attribute — that today's // deploy-time checks would refuse, brought back anyway because it passed the gate // of the day it was deployed and its instances are running under it // (ADR-0177). Warned rather than swallowed: the // model is drifting from what the compiler now asks for, and the next deploy of // it will be refused with the author watching. DeploymentReloadedWithProblems = newEvent("deployment.reloaded_with_problems") // DeploymentDiagramUpdated reports a layout-only adjustment to a deployed // definition's diagram (ADR-0251): the picture the // Operations views draw changed while the process behind it did not. It is on // the audit trail rather than merely in the record's stamp because the change // is visible to everyone looking at that definition — running instances and // finished ones alike — and "why does this diagram look different from last // week" is exactly the question an audit line answers. DeploymentDiagramUpdated = newEvent("deployment.diagram_updated") ScriptWorkerEnabled = newEvent("script_worker.enabled") ScriptWorkerMissing = newEvent("script_worker.binary_missing") CallOverrideSkipped = newEvent("call_override.skipped") CollabParticipantsReaped = newEvent("collab.participants_reaped") PlaygroundSessionsReaped = newEvent("playground.sessions_reaped") )
Everything else the running server reports about itself.
var ( // InboundWatchMinuteOverflowed reports a jira watch whose page filled the batch // limit with issues that all share one minute of its cursor field. JQL compares // timestamps to the minute, so no cursor can separate them: the watch steps past // the minute and skips what it could not read. Warned because that is a delivery // gap — and because standing still instead would silently stop the watch for good. InboundWatchMinuteOverflowed = newEvent("inbound_watch.minute_overflowed") )
The inbound event bridge (ADR-0075/0214).
var LimitIgnored = newEvent("limit.ignored")
Resource budgets (ADR-0291). A budget the environment could not set is the one configuration mistake that must never be silent: what is left standing is a ceiling, and a ceiling nobody knows about is how an installation discovers it at the request that ran out of memory.
Functions ¶
func Setup ¶
Setup points the default logger at w in the given format. Everything the process emits — including lines from dependencies that log through the standard library — then arrives as one stream in one shape.
w is the caller's to compose: the server tees stderr into the bounded buffer behind GET /api/v1/logs, and that keeps working because this writes to the same place.
Types ¶
type Event ¶
type Event struct {
// contains filtered or unexported fields
}
Event is a registered log event name.
It is a struct with an unexported field on purpose. A caller outside this package cannot write Event{name: "made.up"}, so the catalogue below is the complete set of names that can ever be logged — the contract is enforced by the compiler rather than by a review comment (invariant I5, compile don't interpret). The one value an outside caller can forge is the zero Event, and that logs as "unregistered" rather than as an empty field.