Help us shape the future of Go! Take the 2026 Go Developer Survey

halolog

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 28, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

README ΒΆ

HaloLog - Zero-Allocation Logging Framework for Go

Go Version License Performance Zero Allocation

A high-performance logging framework for Go with zero-allocation design, structured logging, and enterprise-grade features.

πŸš€ Key Features

  • Zero-allocation hot path - 23.9 ns/op bare message (~42M lines/sec, measured), 0 B/op everywhere
  • Structured logging - Type-safe field handling with auto-inference
  • Multiple output adapters - Console, file, HTTP, syslog, custom
  • PII masking - Automatic sensitive data protection with regex patterns
  • Field encryption - AES-256-GCM field-level encryption
  • Adaptive sampling - Load-aware log sampling with configurable strategies
  • Alert integration - Slack, PagerDuty, and webhook notifications
  • Configuration management - YAML, JSON, environment variables
  • Thread-safe - Lock-free design with per-P state architecture
  • Compliance-friendly - PII masking + field encryption as building blocks for GDPR/HIPAA-style data-handling policies

πŸ“¦ Installation

go get github.com/go-gen-ecosystem/halolog

πŸš€ Quick Start

Basic Usage
package main

import (
    "github.com/go-gen-ecosystem/halolog"
)

func main() {
    // Get a named logger (singleton, no cleanup needed)
    logger := halolog.GetLogger("my-app")

    // Basic logging
    logger.Info("Server started")
    logger.Debug("Debug message")
    logger.Warn("Warning message")
    logger.Error("Error occurred")

    // With fields
    logger.WithField("user_id", 12345).
        WithField("action", "login").
        Info("User logged in")

    // With error
    logger.WithError(err).
        WithField("operation", "database_query").
        Error("Database operation failed")
}
Field keys: string keys vs. pre-declared keys

HaloLog offers two ways to attach fields; both are correct and both are zero-allocation. Choose based on how hot the path is.

String keys (ergonomic β€” the default). WithField/WithString take a plain string key, escaped inline by the JSON formatter. For the short keys typical of logging this is already fast and allocation-free. Use it everywhere you value convenience.

logger.WithField("user_id", 12345).WithField("action", "login").Info("login")

Pre-declared keys (fastest β€” for the hottest loops). Declare each key once, typically as a package-level var; its escaping is computed a single time and the hot path emits it with one copy and no lookup at all. This is the zerolog/zap "pre-declared field" pattern.

// declared once, reused forever
var (
    userID = halolog.Key("user_id")
    action = halolog.Key("action")
)

logger.Typed().Str(userID, "alice").Str(action, "login").Info("login")
// keyed typed methods: Str, Int, Int64, Float64, Bool, Err, Any

Rule of thumb: reach for halolog.Key(...) in tight, high-frequency logging loops; use string keys everywhere else. Neither allocates on the hot path.

Contextual (child) loggers β€” request-scoped fields for one memcpy

Bind fields once, log them on every line. The bound context is encoded to its final bytes a single time, at Logger() β€” after that, each line emits the whole context as one memcpy, not a per-line re-encode. This is the shape of real service logging (per-request/per-tenant loggers), and the measured scenario where HaloLog leads the field outright (37 ns/op with five bound fields + a call-site field; zerolog 91, zap 180, phuslu 55 on the same host).

reqLog := logger.With().
    Str(keyTenant, "acme").
    WithString("region", "eu-west-1").
    WithInt("shard", 7).
    Logger()

reqLog.Info("accepted")                            // context rides along
reqLog.Typed().WithInt("status", 200).Info("done") // …and composes with fields
child := reqLog.With().WithString("op", "billing").Logger() // chains

Bound fields stay visible to PII masking (they travel as structured data on the masked path β€” binding is never a masking bypass), appear before per-line fields, are capped at 32 per logger, and every line remains 0 allocs/op (guarded). The child inherits the parent's level at derivation.

Schema-checked logging facades (halologgen)

Declare your loggable fields once in a schema; generate a facade where every field is a typed method β€” a misspelled key or wrong-typed value fails the build instead of corrupting a log line. Key escaping is computed at generation time and frozen by a generated test.

# logging.yaml
package: applog
fields:
  - { name: user_id, type: string, ident: UserID }
  - { name: status,  type: int }
go run github.com/go-gen-ecosystem/halolog/cmd/halologgen -schema logging.yaml -out ./applog
log := applog.Wrap(coreLogger)
log.Info().UserID("alice").Status(200).Msg("handled")   // compile-checked
reqLog := log.With().UserID("alice").Logger()           // schema-typed contexts too

Generated facades ride the same hot paths (level-first lines, pre-declared keys, bound contexts) and stay 0 allocs/op. See examples/applog/ for a complete generated package with its determinism and behavior tests.

Level-first lines (cheapest disabled logging)

InfoLine/DebugLine/WarnLine/ErrorLine fix the level when the line opens, so a filtered-out level costs a single check β€” no state, no encoding, zero allocations:

logger.InfoLine().Str(keyUser, "alice").WithInt("status", 200).Msg("handled")
logger.DebugLine().WithString("dump", expensive()).Msg("trace") // ~1ns when Debug is off*

* the level check itself; argument evaluation is still yours to guard.

Using HaloLog from log/slog

Slog-first codebases switch backends with one line β€” all existing slog call sites keep working:

import "github.com/go-gen-ecosystem/halolog/slogbridge"

slog.SetDefault(slog.New(slogbridge.New(logger)))
slog.Info("handled", "status", 200, slog.Group("req", "id", "abc"))
// {"time":"...","level":"INFO","message":"handled","status":200,"req.id":"abc"}

The bridge passes the standard library's testing/slogtest conformance suite (groups are dot-joined; HaloLog stamps its own clock time on every line).

OpenTelemetry trace correlation

The otelbridge module (separate go.mod β€” the core logger stays free of OpenTelemetry dependencies) derives a child logger carrying trace_id and span_id, hex-encoded once at bind time; every line in the request then pays a single memcpy for its correlation fields, at 0 allocs/op:

import "github.com/go-gen-ecosystem/halolog/otelbridge"

func handle(w http.ResponseWriter, r *http.Request) {
    log := otelbridge.Bind(r.Context(), baseLogger) // no span? returns baseLogger
    log.Info("handling")  // …,"trace_id":"4bf9…","span_id":"00f0…"
}
Timestamp precision

The JSON formatter renders whole seconds by default (fastest β€” the header is a single cached memcpy). For trace correlation, pick a sub-second resolution:

f := json.NewJsonFormatterWithPrecision(json.PrecisionMilli) // .123
// PrecisionSecond | PrecisionMilli | PrecisionMicro | PrecisionNano

Every precision is zero-allocation. Honest bound: the default cached clock refreshes every 10ms, so displayed sub-second digits carry up to ~10ms of wall-clock skew β€” sufficient for in-service ordering; run a finer cache.NewCachedClock interval if you need tighter accuracy.

Advanced Configuration
import (
    "github.com/go-gen-ecosystem/halolog"
    "github.com/go-gen-ecosystem/halolog/config"
    "github.com/go-gen-ecosystem/halolog/types"
)

// Create with custom configuration
cfg := &config.ImmutableConfig{
    Level:         types.InfoLevel,
    EnableMetrics: true,
}
logger := halolog.GetLoggerWithConfig("my-app", cfg)

πŸ”Œ Output Adapters

Console
import "github.com/go-gen-ecosystem/halolog/adapters/outputs/console"

adapter := console.New()                          // writes to os.Stdout
// adapter := console.NewWithWriter(w, formatter) // custom writer / formatter
File with Rotation
import "github.com/go-gen-ecosystem/halolog/adapters/outputs/file"

adapter, _ := file.NewFileAdapter("app.log", &file.RotationConfig{
    MaxSize:    100 * 1024 * 1024, // 100MB
    MaxBackups: 10,
    Compress:   true,
})
HTTP/Webhook
import "github.com/go-gen-ecosystem/halolog/adapters/outputs/http"

adapter := http.NewHTTPAdapterWithOptions(&http.HTTPAdapterOptions{
    URL:           "https://logs.example.com/ingest",
    BatchSize:     100,
    FlushInterval: 5 * time.Second,
})
Async ring (low caller latency)

Wrap any destination to move serialization and I/O off the calling goroutine. Producers copy each record into a bounded, lock-free ring and return immediately; a single background goroutine serializes and writes. This optimizes for low, predictable caller latency (not total throughput, which is bounded by the one writer). The record is copied into ring-owned storage, so it is safe even though the logger recycles its entry immediately.

import (
    "os"
    "github.com/go-gen-ecosystem/halolog/adapters/outputs/asyncring"
    jsonfmt "github.com/go-gen-ecosystem/halolog/adapters/formatters/json"
)

adapter, _ := asyncring.New(asyncring.Options{
    Writer:    os.Stdout,
    Formatter: jsonfmt.NewJsonFormatter(),
    Capacity:  1024,             // rounded up to a power of two
    OnFull:    asyncring.Drop,   // or asyncring.Block
})
defer adapter.Close()           // drains everything already accepted
// adapter.Dropped() reports records dropped under overload (OnFull=Drop)

Caveat: field values are captured by shallow copy. Log values, not mutable references β€” a WithField("x", &mutableStruct) may be serialized later by the background goroutine.

Syslog
import "github.com/go-gen-ecosystem/halolog/adapters/outputs/syslog"

adapter, _ := syslog.NewSyslogAdapter("localhost:514", "myapp")

πŸ”’ Security Features

PII Masking
import "github.com/go-gen-ecosystem/halolog/masking"

masker := masking.NewPIIMasker()
masker.AddPattern("email", `\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b`, "[EMAIL]")
masker.AddPattern("ssn", `\b\d{3}-\d{2}-\d{4}\b`, "[SSN]")
Field Encryption
// Initialize global encryptor
masking.InitGlobalEncryptor("your-32-byte-secret-key")

// Use encrypted fields
logger.WithEncryptedField("ssn", "123-45-6789").
    WithEncryptedField("credit_card", "4111-1111-1111-1111").
    Info("Payment processed")

πŸ“Š Sampling

Wire a sampler into the logger with Sampling(...); every Trace–Error line is then offered to ShouldSample before it is written. Fatal and Panic lines are never sampled away β€” the last line before a crash always lands. Configuring a sampler keeps the capture path (it disables the direct-append fast path, since each line must be inspected).

Backpressure sampling (load-shedding with a control loop)

Classic samplers drop a fixed fraction whether or not the pipeline is keeping up. BackpressureSampler closes the loop instead: it reads the async ring's live occupancy and sheds Trace–Warn lines in proportion to how full the pipeline actually is β€” nothing below the low watermark, linearly down to keep-1-in-16 at the high watermark β€” while Error and above always pass (the lines an operator needs most are the ones an overloaded system emits). Decisions are O(1), allocation-free, and deterministic, so drops spread evenly instead of clustering.

ring, _ := asyncring.New(asyncring.Options{Writer: f, Formatter: jsonfmt.NewJsonFormatter(), Capacity: 4096})
logger := core.New().
    Adapter(ring).
    Sampling(sampling.NewBackpressureSampler(ring, 0.5, 0.9)). // watermarks: fill fractions
    MustBuild()
logger := core.New().
    Level(types.InfoLevel).
    Adapter(myAdapter).
    Sampling(sampler).
    MustBuild()
Count-Based Sampling
import "github.com/go-gen-ecosystem/halolog/sampling"

sampler := sampling.NewSamplingManager(sampling.SamplingConfig{
    Strategy:            sampling.SampleByCount,
    SamplingDenominator: 10, // Sample 10%
})
Adaptive Sampling
sampler := sampling.NewAdaptiveSampler(sampling.AdaptiveConfig{
    BaseRate:         0.1,   // 10% base rate
    ErrorBoost:       10.0,  // 10x for errors
    TargetThroughput: 10000, // Adjust based on load
})

🚨 Alert Integration

Slack Alerts
import "github.com/go-gen-ecosystem/halolog/alerts"

sender := alerts.NewSlackSender(alerts.SlackAlertConfig{
    WebhookURL: "https://hooks.slack.com/services/...",
    Channel:    "#alerts",
    RateLimit:  time.Minute,
})

sender.Send(&alerts.AlertPayload{
    Level:   "error",
    Message: "Critical error occurred",
})
PagerDuty
sender := alerts.NewPagerDutySender(alerts.PagerDutyAlertConfig{
    IntegrationKey: "your-integration-key",
    Severity:       "critical",
})

βš™οΈ Configuration Management

YAML Configuration
loader := config.NewConfigLoader()
cfg, err := loader.LoadFromYAML([]byte(`
level: info
output: multi
format: json
file:
  path: app.log
  rotation:
    max_size: 104857600
    max_age: 604800
    compress: true
alerts:
  slack:
    webhook_url: https://hooks.slack.com/services/...
    threshold: error
`))
JSON Configuration
cfg, err := loader.LoadFromJSON([]byte(`{
    "level": "info",
    "output": "multi",
    "format": "json",
    "masking": {
        "enabled": true,
        "patterns": [
            {"name": "email", "pattern": "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b", "mask": "[EMAIL]"}
        ]
    }
}`))
Environment Variables
export HALOLOG_LEVEL=info
export HALOLOG_FORMAT=json
export HALOLOG_OUTPUT=multi
export HALOLOG_FILE_PATH=/var/log/myapp.log

🏁 Performance Benchmarks

Measured on Go 1.27.0, linux/amd64 (the CI environment; Intel Core Ultra 9 285HX), 5 runs Γ— 1s, benchstat medians. Every logger writes a full structured JSON line (timestamp + level + message + fields) to io.Discard via benchmarks/ β€” the committed, fairness-audited comparison suite that now includes phuslu/log, the fastest logger on public Go leaderboards. Run it yourself; numbers vary by machine.

                        HaloLog   phuslu   zerolog     zap     slog   logrus
Bare message            23.9 ns   63.1 ns   86.8 ns  146.2 ns  280.3  1395 ns
One field (typed)       32.0 ns   69.9 ns   99.5 ns  183.7 ns  312.9  1486 ns
Ten fields (typed)      83.6 ns  120.7 ns  201.7 ns  437.4 ns  992.9  4066 ns
Ten fields (keyed)      83.1 ns        β€”        β€”        β€”        β€”       β€”
Twenty fields (typed)  146.0 ns  179.9 ns  230.6 ns  394.6 ns      β€”       β€”
Disabled level         0.83 ns         β€”        β€”        β€”        β€”       β€”
HaloLog allocations    0 B/op, 0 allocs/op in every scenario

Honest summary: HaloLog wins every scenario in this field on both linux/amd64 and windows/amd64 β€” 2.6Γ— ahead of phuslu on bare messages, 2.2Γ— at one field, 44% at ten fields, 23% at twenty on Linux, with the same-run Windows head-to-head also swept β€” at 0 allocs/op everywhere, with fully escaped keys and never-interleaved lines. HaloLog is also the only logger in the field whose latency is stable across operating systems (no per-line clock reads, no syscalls). Full method, fairness notes, and per-platform tables: benchmarks/comprehensive_comparison.md. Every hot path is pinned at 0 allocs/op by seven committed guards (go test ./core -run TestZeroAlloc).

πŸ“Š Comparison with Other Loggers

Feature HaloLogger Zerolog phuslu/log Zap Logrus
Bare message 23.9 ns Β· 0 B 86.8 ns Β· 0 B 63.1 ns Β· 0 B 146.2 ns Β· 0 B 1395 ns Β· 797 B
One field 32.0 ns Β· 0 B 99.5 ns Β· 0 B 69.9 ns Β· 0 B 183.7 ns Β· 64 B 1486 ns Β· 1.5 KiB
Ten fields 83.6 ns Β· 0 B 201.7 ns Β· 0 B 120.7 ns Β· 0 B 437.4 ns Β· 706 B 4066 ns Β· 3.4 KiB
Disabled level 0.8 ns ~1 ns ~1 ns ~2 ns ~15 ns
PII Masking βœ… Built-in ❌ External ❌ External ❌ External ❌ External
File Rotation βœ… Built-in ❌ External βœ… Built-in ❌ External ❌ External
Sampling βœ… Adaptive βœ… Basic ❌ Manual βœ… Basic ❌ Manual
Alerting βœ… Integrated ❌ External ❌ External ❌ External ❌ External
Encryption βœ… Field-level ❌ External ❌ External ❌ External ❌ External
Configuration βœ… Multi-format ❌ Code-only ❌ Code-only ❌ Code-only ❌ Code-only
Fatal and Panic semantics

Fatal(...) writes the line, flushes every adapter, then calls os.Exit(1); Panic(...) writes the line, then panics with the message β€” the same contract as zap, zerolog, logrus, and the standard library. Tests and embedders can intercept termination with core.Config.ExitFunc.

πŸ“š Documentation

πŸ§ͺ Testing

Unit Testing
func TestUserService(t *testing.T) {
    // Create test logger with discard adapter
    logger := halolog.GetLogger("test")

    service := NewUserService(logger)

    // Capture logs for verification
    captured := logger.StartCapture(types.CaptureConfig{
        MaxEntries: 100,
        Filter: func(entry *types.LogEntry) bool {
            return entry.Level >= types.InfoLevel
        },
    })

    // Run your test
    err := service.CreateUser("test@example.com")

    // Verify logs
    captured.Replay(func(entry *types.LogEntry) {
        if entry.Message == "User created" {
            assert.Equal(t, "test@example.com", entry.Fields["email"])
        }
    })
}
Benchmark Testing
func BenchmarkLogging(b *testing.B) {
    logger := halolog.GetLogger("benchmark")

    b.ResetTimer()
    b.ReportAllocs()

    for i := 0; i < b.N; i++ {
        logger.WithField("iteration", i).
            WithField("data", "test data").
            Info("Benchmark message")
    }
}

🀝 Contributing

We welcome contributions! Please see our Contributing Guide for details.

πŸ“„ License

HaloLogger is released under the Apache License 2.0.


πŸš€ HaloLogger - Zero-allocation logging for Go applications

⬆ Back to Top

Documentation ΒΆ

Index ΒΆ

Constants ΒΆ

This section is empty.

Variables ΒΆ

This section is empty.

Functions ΒΆ

func GetLogger ΒΆ

func GetLogger(name string) *core.Logger

GetLogger returns a process-wide singleton logger for the given name, creating a default one (info level) on first use. It is the simplest entry point for applications that just want named logger access.

func GetLoggerWithConfig ΒΆ

func GetLoggerWithConfig(name string, cfg *config.ImmutableConfig) *core.Logger

GetLoggerWithConfig returns a process-wide singleton logger for the given name, built from the supplied immutable configuration on first use.

func Key ΒΆ

func Key(name string) *types.FieldKey

Key declares a reusable field key with its JSON escaping pre-computed once. Declare keys once (typically as package-level vars) and reuse them on the hot path via the typed builder's keyed methods (Str/Int/…), which emit the key with a single copy and never escape it per call:

var userID = halolog.Key("user_id")
log.Typed().Str(userID, "alice").Info("login")

Use pre-declared keys in the hottest logging paths; the plain string-key API (WithField/WithString) is equally correct β€” its keys are escaped inline per call, which for the short keys typical of logging costs a table-driven scan of the key bytes.

func WithAdapters ΒΆ

func WithAdapters(adapters ...types.Adapter) func(*config.ConfigBuilder)

WithAdapters is a convenience function to append adapters to a config builder.

Types ΒΆ

type Logger ΒΆ

type Logger = *core.Logger

Logger is an alias for core.Logger to support fluent API type usage

Directories ΒΆ

Path Synopsis
_parked
clean
Demo main file to run UltraFluent adapter demonstrations
Demo main file to run UltraFluent adapter demonstrations
cli
cmd command
cmd/halolog command
core_old
Package core provides Entry-based API for zero-allocation logging with stack allocation Author: Admilson B. F. Cossa
Package core provides Entry-based API for zero-allocation logging with stack allocation Author: Admilson B. F. Cossa
examples/demo command
v2
adapters
formatters/json
Package json β€” direct-append fast path.
Package json β€” direct-append fast path.
outputs/asyncring
Package asyncring provides an asynchronous log sink backed by a bounded, lock-free multi-producer/single-consumer ring buffer.
Package asyncring provides an asynchronous log sink backed by a bounded, lock-free multi-producer/single-consumer ring buffer.
outputs/console
Package console provides a console/stdout output adapter for HaloLog.
Package console provides a console/stdout output adapter for HaloLog.
cmd
halologgen command
halologgen generates a compile-time-checked logging facade from a schema.
halologgen generates a compile-time-checked logging facade from a schema.
halologgen/internal/gen
Package gen implements the halologgen generator: schema in, a compile-time-checked typed logging facade out.
Package gen implements the halologgen generator: schema in, a compile-time-checked typed logging facade out.
Package core provides the high-performance HaloLog logger implementation.
Package core provides the high-performance HaloLog logger implementation.
examples
applog
Package applog is a compile-time-checked logging facade: every loggable field is a typed method, so a misspelled key or wrong-typed value fails the build instead of corrupting a log line.
Package applog is a compile-time-checked logging facade: every loggable field is a typed method, so a misspelled key or wrong-typed value fails the build instead of corrupting a log line.
otelbridge module
Package slogbridge adapts a HaloLog logger to the standard library's log/slog Handler interface, so slog-first codebases can adopt HaloLog by changing one line:
Package slogbridge adapts a HaloLog logger to the standard library's log/slog Handler interface, so slog-first codebases can adopt HaloLog by changing one line:
Package types β€” optional capability interfaces for the direct-append fast path.
Package types β€” optional capability interfaces for the direct-append fast path.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL