memos/apps/memos-local-plugin/docs/LOGGING.md

6.2 KiB

Logging

The core/logger/ directory implements all logging for the plugin. This document explains how it's wired, what each channel means, what each sink holds, and how retention works.

Quick mental model

emit() → channel-level filter → redactor → fan-out to N sinks → transports → file/console/SSE/buffer
  • One LogRecord shape for everything (agent-contract/log-record.ts).
  • Three orthogonal axes: level (trace…fatal), kind (app/audit/llm/perf/events/error), channel (core.l2.cross-task …).
  • Sinks select on kind; transports do not. Channel-level filtering happens at emit time so every transport in the same sink gets the same set.

Files on disk

~/.<agent>/memos-plugin/logs/
├── memos.log               human-readable main log (rotates by size + day, gzipped)
├── error.log               WARN/ERROR/FATAL across every channel
├── audit.log               永不删除 — monthly gzip rotation only
├── llm.jsonl               every LLM call: provider, model, op, latency, tokens, status
├── perf.jsonl              every `logger.timer(...)` close
├── events.jsonl            every CoreEvent (also broadcast over SSE)
└── self-check.log          startup probe results (tiny)

Retention policy

Sink Rotation Retention
app size + day, gzip last logging.file.retentionDays (default 30)
error size + day, gzip same as app
audit month, gzip 永不删除
llm day, gzip forever (cheap; line-format)
perf day, gzip forever
events day, gzip forever

Audit/LLM/perf/events being kept forever is intentional: those streams enable post-hoc debugging and compliance. They're append-only JSONL, gzipped after each rotation, so disk cost stays modest.

Levels

trace < debug < info < warn < error < fatal. Set the global default in logging.level; override per channel in logging.channels:

logging:
  level: info
  channels:
    "core.l2": debug
    "core.l2.cross-task": trace      # longer prefix wins
    "llm.openai": info

Channels

The full canonical list lives in core/logger/channels.ts. Quick reference:

Prefix Owner module(s)
core.session.* core/session/
core.capture, core.capture.extractor, core.capture.reflection, core.capture.alpha, core.capture.embed core/capture/
core.reward, core.reward.task-summary, core.reward.r-human, core.reward.alpha, core.reward.backprop, core.reward.priority core/reward/
core.memory.l1.* core/memory/l1/
core.memory.l2, core.memory.l2.associate, core.memory.l2.candidate, core.memory.l2.induce, core.memory.l2.gain, core.memory.l2.events core/memory/l2/
core.memory.l3, core.memory.l3.cluster, core.memory.l3.abstract, core.memory.l3.merge, core.memory.l3.confidence, core.memory.l3.feedback, core.memory.l3.events core/memory/l3/
core.episode.* core/episode/
core.feedback, core.feedback.signals, core.feedback.evidence, core.feedback.synthesize, core.feedback.subscriber, core.feedback.events core/feedback/
core.skill, core.skill.crystallize, core.skill.verifier, core.skill.packager, core.skill.subscriber, core.skill.events core/skill/
core.retrieval, core.retrieval.tier1, core.retrieval.tier2, core.retrieval.tier3, core.retrieval.ranker, core.retrieval.injector, core.retrieval.events core/retrieval/
core.pipeline.* core/pipeline/
core.hub.* core/hub/
core.telemetry core/telemetry/
core.update-check core/update-check/
config core/config/
logger.* core/logger/
storage, storage.migration, storage.repos, storage.vector core/storage/
embedding, embedding.* core/embedding/
llm, llm.* core/llm/
server, server.* (incl. server.migrate) server/
bridge, bridge.* bridge/
adapter.openclaw adapters/openclaw/
adapter.hermes adapters/hermes/ (Python forwards through bridge)
system.* startup/shutdown/self-check

When you introduce a new channel, add it to channels.ts AND this table in the same commit.

Redaction

core/logger/redact.ts runs before any transport. Defaults catch:

  • Object keys: api_key, secret, token, password, authorization, auth, cookie, session_token, access_token, refresh_token.
  • Value patterns: Bearer tokens, sk-… keys, JWTs, emails, phone numbers.

Extend in config.yaml:

logging:
  redact:
    extraKeys: ["my_super_secret"]
    extraPatterns: ["INTERNAL-[A-Z]{8}-[0-9]{4}"]

Performance

logger.timer("op") returns a disposable span. Use as:

{
  using span = log.timer("retrieval.tier1");
  await doTier1();
}

Sample rate (logging.perfLog.sampleRate, default 1.0) only controls whether the perf record is emitted; the timer math always runs.

SSE broadcast

Every record (post-redaction) is also pushed to a SseBroadcastTransport that server/sse.ts subscribes to. The viewer's Logs tab consumes that stream. Filtering happens client-side over the firehose.

Adding a new sink

  1. Subclass Sink in core/logger/sinks/.
  2. Wire the sink in initLogger (core/logger/index.ts).
  3. Add a row to the Files on disk table above.
  4. Add a row to core/logger/retention.ts.
  5. Update tests/unit/logger/.

Hermes / Python forwarding

adapters/hermes/memos_provider/log_forwarder.py serializes each Python logging.LogRecord into our LogRecord shape and sends it through the bridge (logs.forward notification — see agent-contract/jsonrpc.ts). The bridge calls rootLogger.forward(record) which bypasses the level gate (Python has already filtered) but still passes through redaction.