Skip to content

rbjorklin/ocaml-message-templates

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

92 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OCaml Message Templates

PPX-based library for compile-time validated message templates with structured logging.

⚠️ This project is fully AI generated and therefore will surely include some inaccuracies or straight up lies. The owner of this repo does not suggest anyone use this in production as it relies heavily on Obj usage. The library has known problems like the fact that it cannot distinguish between bools and integers of the value 0 or 1 because of how they are represented by the runtime. More complex types like Maps and Sets also end up with very verbose and unclear output formats. ⚠️

Features

  • Compile-time template validation with automatic variable capture from scope
  • Type-safe logging with hard compile errors for undefined variables
  • Dual output: formatted strings and structured JSON (CLEF format)
  • PPX-driven code generation with zero runtime template parsing overhead
  • Operators for structure preservation (@) and stringification ($)
  • Printf-compatible format specifiers (:05d, .2f, etc.) and alignment
  • Six log levels with comparison operators
  • Multiple sinks: Console, File (with rolling), JSON, Composite, Null
  • Context tracking with ambient properties and correlation IDs
  • Level-based and property-based filtering
  • Lwt and Eio async logging support

Installation

Core Library

opam install message-templates message-templates-ppx

Async Support (Optional)

For Lwt (monadic concurrency):

opam install message-templates-lwt

For Eio (effects-based concurrency):

opam install message-templates-eio

dune-project Dependencies

(depends
  (ocaml (>= 5.4.0))
  message-templates
  message-templates-ppx
  ;; Optional: choose one or both
  (message-templates-lwt (>= 0.1.0))
  (message-templates-eio (>= 0.1.0))
  yojson
  ptime
  unix)

Usage

Add the PPX to your dune file:

(executable
 (name myapp)
 (libraries message-templates yojson unix)
 (preprocess (pps message-templates-ppx)))

Template Basics

let () =
  let username = "alice" in
  let ip_address = "192.168.1.1" in

  (* Template with automatic variable capture *)
  let msg, json = [%template "User {username} logged in from {ip_address}"] in

  Printf.printf "%s\n" msg;
  (* Output: User alice logged in from 192.168.1.1 *)

  Yojson.Safe.to_string json |> print_endline;
  (* Output: {"@t":"2026-01-31T23:54:42-00:00","@mt":"User {username} logged in from {ip_address}",
               "@m":"User alice logged in from 192.168.1.1","username":"alice","ip_address":"192.168.1.1"} *)

Logging Basics

Configure the global logger:

open Message_templates

let () =
  (* Setup logger at application startup *)
  let logger =
    Configuration.create ()
    |> Configuration.minimum_level Level.Information
    |> Configuration.write_to_console ~colors:true ()
    |> Configuration.write_to_file ~rolling:File_sink.Daily "app.log"
    |> Configuration.create_logger
  in
  Log.set_logger logger

Log messages with variables:

let process_user user_id =
  Log.information "Processing user {user_id}" [("user_id", `Int user_id)];

  try
    (* ... work ... *)
    Log.debug "User {user_id} processed successfully" [("user_id", `Int user_id)]
  with exn ->
    Log.error "Failed to process user {user_id}" [("user_id", `Int user_id)]

Async Logging

Lwt Support (Monadic Concurrency)

open Message_templates
open Message_templates_lwt
open Lwt.Syntax

let main () =
  (* Setup async logger *)
  let logger =
    Configuration.create ()
    |> Configuration.minimum_level Level.Information
    |> Configuration.write_to_console ~colors:true ()
    |> Lwt_configuration.create_logger
  in

  (* All log methods return unit Lwt.t *)
  let* () = Lwt_logger.information logger "Server starting on port {port}" [("port", `Int 8080)] in

  (* Clean up *)
  Lwt_logger.close logger

let () = Lwt_main.run (main ())

Eio Support (Effects-Based Concurrency)

open Message_templates
open Message_templates_eio

let run ~stdout ~fs =
  Eio.Switch.run @@ fun sw ->
  (* Setup Eio logger - requires switch for fiber management *)
  let logger =
    Configuration.create ()
    |> Configuration.minimum_level Level.Information
    |> Configuration.write_to_console ~colors:true ()
    |> Eio_configuration.create_logger ~sw
  in

  (* Synchronous logging - waits for completion *)
  Eio_logger.information logger "Server starting" [];

  (* Fire-and-forget logging - runs in background fiber *)
  Eio_logger.information_async logger "Background task started" [];

  (* Your Eio code here *)
  ()

let () = Eio_main.run @@ fun env -> run ~stdout:env#stdout ~fs:env#fs

PPX Logging

Use PPX extensions for even cleaner syntax:

let user = "alice" in
let action = "login" in

(* All six log levels supported *)
[%log.verbose "Detailed trace: user={user}, action={action}"];
[%log.debug "Debug info: user={user}"];
[%log.information "User {user} performed {action}"];
[%log.warning "Warning for user {user}"];
[%log.error "Error for user {user}"];
[%log.fatal "Fatal error for user {user}"];

Contextual Logging

Track request context across function calls:

let handle_request request_id user_id =
  Log_context.with_property "RequestId" (`String request_id) (fun () ->
    Log_context.with_property "UserId" (`Int user_id) (fun () ->
      Log.information "Request started" [];

      (* All logs within this scope include RequestId and UserId *)
      validate_request ();
      process_data ();

      Log.information "Request completed" []
    )
  )

Correlation IDs

For distributed tracing, use correlation IDs:

(* Generate and use a correlation ID automatically *)
Log_context.with_correlation_id_auto (fun () ->
  Log.information "Processing request" [];
  (* All logs include correlation ID *)
  call_external_service ();
  Log.information "Request completed" []
);

(* Or use a specific correlation ID *)
Log_context.with_correlation_id "req-abc-123" (fun () ->
  (* Logs include @i field with correlation ID *)
  process_request ()
)

Configuration Options

let logger =
  Configuration.create ()
  |> Configuration.debug  (* Set minimum level *)

  (* Console with colors *)
  |> Configuration.write_to_console
       ~colors:true
       ~stderr_threshold:Level.Warning
       ()

  (* File with daily rolling *)
  |> Configuration.write_to_file
       ~rolling:File_sink.Daily
       ~output_template:"{timestamp} [{level}] {message}"
       "logs/app.log"

  (* Static properties *)
  |> Configuration.enrich_with_property "AppVersion" (`String "1.0.0")
  |> Configuration.enrich_with_property "Environment" (`String "Production")

  (* Filters *)
  |> Configuration.filter_by_min_level Level.Information

  |> Configuration.create_logger

Operators

  • {var} - Default: Standard variable substitution
  • {@var} - Structure: Preserve as JSON structure
  • {$var} - Stringify: Convert value to string representation

Format Specifiers

Format specifiers work like Printf formats:

let count = 42 in
let score = 98.5 in
let active = true in

let msg, _ = [%template "Count: {count:05d}, Score: {score:.1f}, Active: {active:B}"] in
(* Output: Count: 00042, Score: 98.5, Active: true *)

Common format specifiers:

  • {var:d} - Integer (decimal)
  • {var:05d} - Integer with zero-padding
  • {var:f} - Float
  • {var:.2f} - Float with 2 decimal places
  • {var:B} - Boolean
  • {var:s} - String (default)

Alignment

Control field width and alignment:

let name = "Alice" in
let status = "active" in

[%template "|{name,10}|{status,-10}|"]
(* Output: |     Alice|active    | *)

Escaped Braces

Use doubled braces for literal braces:

let msg, _ = [%template "Use {{braces}} for literals"] in
(* Output: Use {braces} for literals *)

Architecture

The PPX rewriter operates at compile time:

  1. Parse template string into parts using Angstrom
  2. Validate variable existence against lexical scope
  3. Generate OCaml code for string and JSON output
  4. Zero runtime parsing overhead

Logging Pipeline

Synchronous:

Application
    |
    v
Level Check
    |
    v
Template Expansion (PPX)
    |
    v
Context Enrichment
    |
    v
Filtering
    |
    v
Sinks

JSON Output Structure

Log events follow the CLEF format:

{
  "@t": "2026-01-31T23:54:42-00:00",
  "@mt": "User {username} logged in from {ip_address}",
  "@m": "User alice logged in from 192.168.1.1",
  "@l": "Information",
  "username": "alice",
  "ip_address": "192.168.1.1"
}
  • @t: RFC3339 timestamp
  • @mt: Message template
  • @m: Rendered message
  • @l: Log level
  • @i: Correlation ID (optional)
  • Additional fields: Template variables and context properties

Performance

Benchmark results (1 million iterations):

PPX Simple Template:  0.090s (11.2M ops/sec)
Printf Simple:        0.058s (17.1M ops/sec)
String Concat:        0.034s (29.8M ops/sec)

PPX with Formats:     0.431s (2.32M ops/sec)
Printf with Formats:  0.387s (2.58M ops/sec)

PPX JSON Output:      0.334s (2.99M ops/sec)

PPX-generated code has minimal overhead compared to hand-written Printf.


Testing

Run the test suite:

dune runtest

This runs tests across all packages:

  • Core library: Level, Sink, Logger, Configuration, Global log, PPX, Parser, Circuit breaker, Metrics tests
  • Lwt package: Lwt logger and sink tests
  • Eio package: Eio logger and sink tests

Examples

See the examples/ directory:

  • basic.ml - Simple template usage
  • logging_basic.ml - Basic logging setup and usage
  • logging_advanced.ml - Multiple sinks, rolling files, enrichment
  • logging_ppx.ml - PPX extension usage
  • logging_clef_ppx.ml - PPX with pure JSON CLEF output
  • logging_clef_json.ml - Structured JSON logging

Run examples:

# Core examples
dune exec examples/basic.exe
dune exec examples/logging_basic.exe
dune exec examples/logging_advanced.exe
dune exec examples/logging_ppx.exe
dune exec examples/logging_clef_ppx.exe
dune exec examples/logging_clef_json.exe

API Reference

Core Modules

  • Level - Log levels with comparison operators
  • Log_event - Log event type
  • Template_parser - Template parsing
  • Types - Core types

Sinks

  • Console_sink - Console output with colors
  • File_sink - File output with rolling
  • Json_sink - CLEF/JSON output
  • Composite_sink - Multi-sink routing
  • Null_sink - Discard events

Logging

  • Logger - Logger interface
  • Filter - Event filters
  • Configuration - Configuration builder
  • Log - Global logger
  • Log_context - Ambient context

Reliability

  • Circuit_breaker - Error recovery
  • Metrics - Per-sink performance tracking
  • Timestamp_cache - Timestamp caching
  • Shutdown - Graceful shutdown

Lwt (message-templates-lwt)

  • Lwt_logger, Lwt_configuration, Lwt_file_sink, Lwt_console_sink

Eio (message-templates-eio)

  • Eio_logger, Eio_configuration, Eio_file_sink, Eio_console_sink

Compliance

Implements the Message Templates specification:

  • Named property holes: {name}
  • Positional property holes: {0}, {1}
  • Escaped braces: {{ and }}
  • Operators: @ for structure, $ for stringification
  • Format specifiers: :format syntax
  • Alignment specifiers: ,width syntax
  • CLEF output with @t, @mt, @m, @l fields

License

MIT

Acknowledgments

This implementation follows the Message Templates specification from https://messagetemplates.org/ and is inspired by Serilog's design patterns.

About

Experimental message-templates library completely generated by AI

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages