Documentation
¶
Overview ¶
Package logging is the suite's log line builder.
Products write JSON lines to stderr; a collector agent ships them onward. This package opens no socket, holds no buffer and rotates no file, because an agent survives the process dying and a library buffer does not.
What it does own is what may appear on a line. Values are typed and sanitized, so a field cannot forge a line the way a bare newline in a value does. Keys must be declared, so a secret cannot ride along under a name nobody anticipated — the suite's two existing redaction schemes are a key-name denylist, which fails silently, and a runtime allowlist, which this borrows and makes stricter.
Index ¶
- Variables
- func AuditFields(fs ...Field) []string
- func DeclareBool(name string) func(bool) Field
- func DeclareInt(name string) func(int64) Field
- func DeclareString(name string) func(string) Field
- func DeclareTime(name string) func(time.Time) Field
- func WithRequestID(ctx context.Context, id string) context.Context
- type Config
- type Event
- type Field
- type Logger
Constants ¶
This section is empty.
Variables ¶
var ( Started = DeclareEvent("started", "service started", slog.LevelInfo) Stopped = DeclareEvent("stopped", "service stopped", slog.LevelInfo) ConfigLoaded = DeclareEvent("config_loaded", "configuration loaded", slog.LevelInfo) AuthSucceeded = DeclareEvent("auth_succeeded", "authentication succeeded", slog.LevelInfo) AuthFailed = DeclareEvent("auth_failed", "authentication failed", slog.LevelWarn) AuthLocked = DeclareEvent("auth_locked", "account locked after repeated failures", slog.LevelWarn) SessionCreated = DeclareEvent("session_created", "session created", slog.LevelInfo) SessionRevoked = DeclareEvent("session_revoked", "session revoked", slog.LevelInfo) MFAChallenged = DeclareEvent("mfa_challenged", "second factor requested", slog.LevelInfo) MFAFailed = DeclareEvent("mfa_failed", "second factor rejected", slog.LevelWarn) RateLimited = DeclareEvent("rate_limited", "request rate limited", slog.LevelWarn) AdminAction = DeclareEvent("admin_action", "administrative action performed", slog.LevelWarn) KeyCreated = DeclareEvent("key_created", "key created", slog.LevelInfo) KeyRotated = DeclareEvent("key_rotated", "key rotated", slog.LevelInfo) CapsuleSealed = DeclareEvent("capsule_sealed", "capsule sealed", slog.LevelInfo) CapsuleOpened = DeclareEvent("capsule_opened", "capsule opened", slog.LevelInfo) CapsuleOpenFailed = DeclareEvent("capsule_open_failed", "capsule failed to open", slog.LevelWarn) RecoveryCodeRedeemed = DeclareEvent("recovery_code_redeemed", "recovery code redeemed", slog.LevelWarn) AuditChainBroken = DeclareEvent("audit_chain_broken", "audit chain failed verification", slog.LevelError) )
The starter set, drawn from what the nine products log today. A product adds its own with DeclareEvent.
var ( Route = DeclareString("route") Method = DeclareString("method") Status = DeclareInt("status") DurationMS = DeclareInt("duration_ms") Bytes = DeclareInt("bytes") Outcome = DeclareString("outcome") UserID = DeclareString("user_id") DeviceID = DeclareString("device_id") ContainerID = DeclareString("container_id") ObjectID = DeclareString("object_id") AttachmentID = DeclareString("attachment_id") UploadID = DeclareString("upload_id") SessionID = DeclareString("session_id") AuditID = DeclareString("audit_id") Count = DeclareInt("count") ReasonCode = DeclareString("reason_code") RetryAfterS = DeclareInt("retry_after_s") Version = DeclareString("version") ErrorKind = DeclareString("error_kind") ErrorText = DeclareString("error_text") RemoteIP = DeclareString("remote_ip") UserAgent = DeclareString("user_agent") Action = DeclareString("action") TargetID = DeclareString("target_id") ActorID = DeclareString("actor_id") CapsuleID = DeclareString("capsule_id") Zone = DeclareString("zone") QName = DeclareString("qname") )
The shared vocabulary. The first nineteen are kynotes-server's allowlist, which is the only field allowlist in the suite and was chosen against a real product; the rest are what the other eight log today. Two of kynotes' keys are absent because they are reserved here: event comes from the event constant and request_id from the context.
Functions ¶
func AuditFields ¶
AuditFields renders declared fields as the canonical key=value strings a chain digests. Order is the argument order, and the digest depends on it.
Audit recomputes this over the fields it is given and compares the result against the record's own Fields, since the two are built at separate call sites — the digest at the chain.Append that minted rec, the flat keys at the Audit call that follows — and nothing but that comparison stops them silently disagreeing.
func DeclareBool ¶
DeclareBool admits a boolean-valued key and returns its constructor.
func DeclareInt ¶
DeclareInt admits an integer-valued key and returns its constructor.
func DeclareString ¶
DeclareString admits a string-valued key and returns its constructor. Call it once, at package level, in the product that needs the key.
func DeclareTime ¶
DeclareTime admits a time-valued key and returns its constructor.
func WithRequestID ¶
WithRequestID puts a request identifier on the context. The logger emits it on every line drawn from that context.
Whether an inbound X-Request-Id header may be trusted is the product's HTTP layer to decide, because that is where the proxy configuration lives.
Types ¶
type Config ¶
type Config struct {
App string // required; identifies the product on every line
Level slog.Leveler // default is slog.LevelInfo when nil, the zero value; a *slog.LevelVar works
Out io.Writer // default os.Stderr
}
Config is everything the logger needs. There is no format field, because there is one format: JSON lines. A syslog renderer on stderr would duplicate a frame the collector agent supplies for us.
func FromEnv ¶
FromEnv reads KY_LOG_LEVEL. App and Out are still the caller's to set, so configuration stays visible at the call site rather than hiding in the library.
It returns an error rather than falling back, because a misspelled level that silently means info is how a product ends up logging nothing useful during the incident it was turned up for.
type Event ¶
type Event struct {
// contains filtered or unexported fields
}
Event is what happened. It carries its name, its message and its level, so all three agree across the suite and none of them is supplied at the call site.
There is deliberately no free-text message parameter anywhere in this package. One would be the last remaining channel for an accidental leak — Log(ctx, AuthFailed, fmt.Sprintf("user %s failed", email)) defeats every other rule here — and a constant message also means the same event reads the same way in every product.
type Field ¶
type Field struct {
// contains filtered or unexported fields
}
Field is one key and value on a log line. Its members are unexported, so the only fields that exist are ones a constructor in this package produced.
func Err ¶
Err reports what kind of error occurred without its message.
It unwraps to the deepest error and emits that one's text, which for a sentinel such as fs.ErrNotExist or auditchain.ErrBrokenChain is a constant. Wrapping is where the dynamic content gets added — filesystem paths, query text, occasionally a token in a URL — so dropping the wrappers drops the leak. This is not airtight: a leaf built by fmt.Errorf with no %w carries whatever the caller put in it. It removes the accidental case, which is the one that happens.
errors.Join (and fmt.Errorf with multiple %w verbs) does not fit the single-chain walk: errors.Unwrap returns nil for one, so a naive loop would stop at err.Error() and emit every joined error's full text — the wrapper this function exists to drop. Instead each joined branch is unwrapped to its own leaf and the leaf kinds are joined, so a joined error leaks no more than a single wrapped one does.
The unwrap walk is bounded at 100 steps total, shared across every branch of a joined error, to guard against a cyclic error chain hanging the goroutine. Real wrap chains are under ten deep, so 100 is a guard rather than a policy.
type Logger ¶
type Logger struct {
// contains filtered or unexported fields
}
Logger writes the suite's log lines. It holds two slog loggers because facility is constant per handler, which keeps it out of reach of a raw slog attribute.
func (*Logger) Audit ¶
Audit records a chain record on the authpriv facility, alongside the same values as flat keys — unless fs disagrees with what rec's Fields digested, in which case the flat keys are withheld and audit_fields_mismatch marks the line instead of silently shipping a flat user_id next to a fields entry that authenticates a different one. The record itself — seq, prev, hash, fields — always ships either way: it is what is authenticated, and withholding it on a caller's mistake would cost the one thing meant to survive one.
Audit ignores the configured level: it writes the handler directly rather than going through LogAttrs, so no KY_LOG_LEVEL can suppress it. An audit record is evidence, not verbosity — chain.Append has already advanced the sequence by the time Audit is called, and a gap left by a dropped line is indistinguishable from tampering. A verbosity knob is not allowed to manufacture a tamper alarm.
The export handed to kyauditverify must be filtered to lines carrying hash: Log and Security lines decode into a zero auditchain.Record without a JSON error, which VerifyStream cannot tell apart from an attack.
The record's storage stays with the product: auditchain deliberately owns none, and a tamper-evident chain that exists only on a machine the attacker may also own is not evidence. This ships a copy; the product keeps writing its local chained file.
The write error is returned, not dropped. A closed stderr, a pipe the collector has stopped draining, a short write: each loses the shipped line, and a missing line is indistinguishable from tampering at central verification. Whoever can stall the collector must not be able to suppress evidence with nothing noticing, so the caller gets to say what happens -- TestAuditReturnsTheWriteError.
func (*Logger) Handler ¶
Handler returns the handler as a slog.Handler, for a product mid-migration to install with slog.SetDefault. Attributes outside the vocabulary are dropped and counted rather than emitted, so the loss shows up on the line instead of being silent.