Chronicles Structured Logging for Nim

repository·master·Indexed 41 days ago

https://github.com/status-im/nim-chronicles

A structured logging library for Nim that focuses on event records with arbitrary properties rather than formatted text strings. It supports multiple output formats including JSON, textlines, and textblocks, and provides advanced scoping mechanisms via logScope, publicLogScope, and dynamicLogScope for attaching properties to logs.

Tokens
4.7K
Snippets
16
Records
22
Agent score
2%

What's inside Chronicles

  1. Define independent log streams with `chronicles_streams`

    master

    While sinks define destinations, chronicles_streams allows you to create multiple independent streams of events, each with its own set of sinks. Each logging statement is associated with exactly one stream.

    Syntax Example: textlog[textlines],transactions[json[file(transactions.json)]]

    This creates two streams:

    1. textlog: The default stream for unqualified logging statements.
    2. transactions: A named stream used via transactions.info ....

    Usage in code:

    textlog.debug "about to create a transaction"
    transactions.info "transaction created", buyer = alice, seller = bob
  2. How `dynamicLogScope` works (Dynamic Scoping)

    master

    The dynamicLogScope construct attaches properties to all logging statements executed anywhere within the call tree originating from the block.

    Unlike logScope, which is bound to the code structure (lexical), dynamicLogScope follows the execution flow. This means it can attach properties to logging statements inside other modules that are called within the block.

    Key differences from logScope:

    • Scope: Affects the entire call tree (dynamic), not just the local block (lexical).
    • Evaluation: Expressions at the start of a dynamicLogScope are eagerly evaluated before the block is entered, whereas logScope expressions are lazily evaluated per log statement.
    proc onNewRequest(req: Request) =
      let reqID = generateID()
      info "request received", reqID
      
      dynamicLogScope(reqID):
        # All logs in handleRequest() and any functions it calls 
        # (even in other modules) will include the reqID property.
        handleRequest(req)
  3. Log from `{.noSideEffect.}` procs using `trace`

    master
    Standard logging statements (like info, error, etc.) are considered side effects and cannot be used inside {.noSideEffect.} procedures or func blocks. However, the trace statement is specifically designed as a debugging aid and is permitted in these contexts.
  4. How `publicLogScope` works (Global Scoping)

    master

    A publicLogScope allows you to define custom properties that affect your entire program, even across different modules.

    Best Practice: The Proxy Module Pattern To avoid symbol ambiguity (like activeChroniclesScope), do not import chronicles directly in your application modules. Instead:

    1. Create a proxy module (e.g., logging.nim) that imports chronicles.
    2. Define a publicLogScope within that module.
    3. Import your proxy module in all other parts of your application.

    You can use the Nim --import: option to enforce that your application only uses this proxy module instead of importing chronicles directly.

    # logging.nim
    import chronicles
    
    proc getServerId*()
    
    publicLogScope:
      serverId = getServerId()
  5. Log rotation and archival in Chronicles

    master

    Chronicles does not natively implement log rotation or archival. Following the 12-factor app methodology, it is recommended to send log output to stdout and allow a supervising daemon (like systemd or Docker) to handle rotation and archival.

    While Chronicles provides file sinks, users requiring rotation should rely on external tools or wait for future integrations with Nim packages that provide low-level abstractions for automatically rotated log files.

  6. How `logScope` works (Lexical Scoping)

    master

    The logScope construct introduces properties that are automatically attached to all logging statements within the current lexical scope.

    Key characteristics:

    • Static properties: You can assign constants (e.g., topics = "rendering") to be attached to all logs in that scope.
    • Dynamic properties: You can assign expressions (e.g., memoryUsage = currentMemUsage()) that are lazily evaluated on every log statement within the scope.
    • Scope isolation: Only logging statements in the same lexical scope are affected.
    • Filtering: The topics property is special; if present, it appears first in formatted output and is used for log filtering.
    logScope:
      topics = "rendering opengl"
      memoryUsage = currentMemUsage() # Evaluated on every log statement
    
    proc renderFrame() = 
      logScope:
        frame = frameCounter # Only affects logs inside this specific block
      debug "Frame started"
  7. Disable Chronicles logging at compile-time

    master

    To completely remove all logging code from your final binary to minimize overhead, use the chronicles_enabled flag set to off during compilation.

    nim c -d:chronicles_enabled=off myprogram.nim
    nim c -d:chronicles_enabled=off myprogram.nim
  8. Enable runtime log filtering

    master

    To control logging behavior while the program is running, you must first enable runtime filtering at compile-time using the chronicles_runtime_filtering flag.

    Enable at compile-time:

    nim c -d:chronicles_runtime_filtering:on myprogram.nim

    Runtime Control API: Once enabled, use the following procedures to control output. Note that runtime filtering happens before evaluating log parameters, so expensive function calls in log statements will not be executed if the log is filtered out.

    proc setLogEnabled*(enabled: bool)
    proc setLogLevel*(level: LogLevel)
    proc setTopicState*(name: string, newState: TopicState, logLevel = LogLevel.NONE): bool

    Types:

    type
      LogLevel = enum
        NONE, TRACE, DEBUG, INFO, NOTICE, WARN, ERROR, FATAL
      TopicState = enum
        Normal, Enabled, Required, Disabled

    Per-sink control: If using multiple sinks, you can target a specific sink by index:

    setLogEnabled(false, sinkIdx: 1)
    setLogLevel(LogLevel.WARN, sinkIdx: 0)
    setLogEnabled(false)
    info "test", value = foo() # foo() will not be called
  9. Use Chronicles for structured logging

    master

    Chronicles provides logging statements for different severity levels. Instead of formatted strings, logs are treated as event records with arbitrary key-value properties.

    Available severity levels:

    • trace
    • debug
    • info
    • notice
    • warn
    • error
    • fatal

    When passing arguments, you can provide key-value pairs (e.g., bytes = page.len). As a shortcut, passing a variable name alone will create a key with the same name as the variable (e.g., passing foo results in foo = foo).

    import chronicles
    
    info "Retrieved page", bytes = page.len
    
    # Shortcut: passing variable name directly
    let foo = 42
    debug "Value of foo", foo
  10. Configure log colors with `chronicles_colors`

    master

    Control the color scheme for human-readable text formats (like textblocks or textlines) sent to standard output.

    Supported Values:

    • auto or AutoColors (Default): Uses colors if at least one destination is a terminal; respects the NO_COLOR=1 environment variable.
    • ansi or AnsiColors: Always emits ANSI escape codes (even for files).
    • no or NoColors: Produces color-less output. This is the default for file sinks and JSON format.
  11. Filter logs by topic at compile-time

    master

    Chronicles allows you to include or exclude logs based on associated topics using compile-time flags. Topics are case-sensitive strings.

    • chronicles_enabled_topics: Only logs matching these topics (or any in the list) are kept. All others are erased.
    • chronicles_required_topics: Only logs containing ALL specified topics are kept.
    • chronicles_disabled_topics: A blacklist; logs with these topics are erased. This takes precedence over enabled/required lists.

    Topic Log Levels: You can specify a minimum log level for a topic using a colon: TopicName:LogLevel. This overrules the global chronicles_log_level for that topic.

    Example CLI flag: -d:chronicles_enabled_topics:MyTopic:DEBUG,AnotherTopic:5 (where 5 is the integer value for a log level).