log

package
v2.0.0 Latest Latest
Warning

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

Go to latest
Published: Apr 20, 2026 License: MIT Imports: 8 Imported by: 3

README

Log Package

The log package acts as a helpful abstraction around the slog Logger that is built into the standard library in Go.

Engineers in GitLab must use this package in order to instantiate loggers for their application to ensure that there is consistency across all of our services in how we emit logs from our systems.

Usage

// returns a new JSON logger that outputs logs to the stdout
logger := log.New()

// A standard Info line
// Note: We strongly encourage that you use the *Context methods
// for observability purposes to ensure you'll be benefiting from
// field enrichment.
logger.InfoContext(ctx, "some info")
Environment variables

The logger can be configured via environment variables, which is the recommended approach for cloud-native deployments.

Values are case-insensitive. If you set an unrecognised value, the logger prints an error message to stderr and falls back to the default.

Variable Values Default Description
GITLAB_LOG_FORMAT json, text json Controls the output format. Use text for human-readable output during local development.
GITLAB_LOG_LEVEL DEBUG, INFO, WARN, ERROR INFO Sets the minimum log level. Values are case-insensitive.
GITLAB_LOG_TIMEZONE IANA timezone names (e.g., UTC, America/New_York) UTC Sets the timezone for log timestamps. Invalid timezones fall back to UTC with a warning.

For example, in a Kubernetes deployment you might set:

env:
  - name: GITLAB_LOG_FORMAT
    value: json
  - name: GITLAB_LOG_LEVEL
    value: WARN

Or locally, when debugging:

GITLAB_LOG_FORMAT=text GITLAB_LOG_LEVEL=DEBUG ./my-service
Programmatic configuration

If you need finer-grained control, you can configure the logger in code. Values set here take precedence over the corresponding environment variables.

level := slog.LevelError
logger := log.NewWithConfig(&log.Config{
    // log.WithWriter - allows you to pass in a custom io.Writer
    // should you wish.
    Writer: os.Stderr,
    // allows you to define the minimum logging level for said logger.
    // When set, this overrides GITLAB_LOG_LEVEL.
    LogLevel: &level,
    // allows you to set the output to text format
    UseTextFormat: true,
})
Timestamp behavior

By default, all timestamps emitted by the logger are in UTC to ensure consistency across GitLab services running in different timezones. This is a deliberate design choice that makes correlation and analysis of logs across distributed systems straightforward.

// Timestamps are automatically in UTC
logger := log.New()
logger.Info("message")  // timestamp will be in UTC, e.g., "2026-04-13T11:15:30Z"
Configuring timezone

If your service needs timestamps in a specific timezone, you can configure it via environment variable or programmatic config:

Via environment variable (recommended for cloud-native deployments):

# Valid IANA timezone names: UTC, America/New_York, Europe/London, Asia/Tokyo, etc.
export GITLAB_LOG_TIMEZONE=America/New_York

Via programmatic configuration:

ny, _ := time.LoadLocation("America/New_York")
logger := log.NewWithConfig(&log.Config{
    Location: ny,
})
logger.Info("message")  // timestamp will be in New York timezone
Custom clock for testing

If you need to use a custom clock (for testing or special use cases), you can provide one via Config.Clock:

// Custom clock for testing
logger := log.NewWithConfig(&log.Config{
    Clock: func() time.Time {
        return time.Date(2026, 4, 13, 12, 0, 0, 0, time.UTC)
    },
})

Note: When using a custom clock with a specific timezone:

ny, _ := time.LoadLocation("America/New_York")
logger := log.NewWithConfig(&log.Config{
    Clock: func() time.Time {
        return time.Now()  // Clock can return any time
    },
    Location: ny,  // Timestamp will be converted to this timezone
})
Writing to Files

If you need to write to specific files, you can achieve this like so:

logger, logCloser, err := log.NewWithFile(filePath, &log.Config{...})
if err != nil {
    // When an error occurs while opening a filePath NewWithFile() will return
    // an stderr logger alongside an error.
    // You can choose how to proceed, here we return the error.
    return err
}

// You are responsible for closing the log file.
defer logCloser.Close()

logger.Info("hello!")

This would create or append logs to a file at filePath file.

Testing Your Observability

The logs that our systems output represent an often-neglected part of our API. Additional reporting systems and alerts are typically built on top of log lines and a lack of testing makes these setups rather fragile in nature.

It's strongly encouraged that all engineers bake in some form of assertions on the logs that they rely on for additional observability configuration within their tests.

// NewWithRecorder returns a logRecorder struct that
// captures all log lines emitted by the `logger`
logger, recorder := logtest.NewWithRecorder(nil)

// We can then perform assertions on things like how many log lines
// have been emitted
assert.Len(t, recorder.Records, 1)

// As well as the shape of individual log lines
assert.Equal(t, "test message", recorder.Records[0].Message)
assert.Equal(t, tt.expectedLevel, recorder.Records[0].Level.String())
assert.Contains(t, recorder.Records[0].Attrs, slog.Attr{Key: "key", Value: slog.AnyValue("value")})

These log lines are captured in a Records field which is a slice of type testRecord:

// testRecord - a representation of a single log message
type testRecord struct {
    Level   slog.Level
    Message string
    Attrs   []slog.Attr
}

Structured Logging with Fields

To emit important information with every log message, use the logger's .With() method to attach attributes. This is the canonical pattern for request-scoped enrichment throughout your request lifecycle.

logger := log.New()

// Create a logger enriched with request-level fields
enriched := logger.With(
    slog.String(fields.GitLabUserName, "1234abc"),
    slog.String(fields.CorrelationID, "req-xyz"),
)

// All logs from this logger will include these fields
enriched.Info("processing request")
enriched.Info("completed request")
// Both lines will include gl_user_name and correlation_id
Request Lifecycle Pattern

For services that handle requests, create an enriched logger at the request boundary and pass it through your execution flow:

// HTTP handler - request boundary
func handleRequest(w http.ResponseWriter, r *http.Request) {
    baseLogger := log.New()

    // Create a logger enriched with request-level fields
    requestLogger := baseLogger.With(
        slog.String("request_id", r.Header.Get("X-Request-ID")),
        slog.String("user_id", extractUserID(r)),
    )

    // Pass the enriched logger through your call stack
    if err := handleBusiness(requestLogger); err != nil {
        requestLogger.Error("request failed", slog.Any("error", err))
    }
    requestLogger.Info("request completed")
}

// Business logic - enrich further if needed
func handleBusiness(logger *slog.Logger) error {
    // Add business-specific fields by creating a new logger from the passed logger
    opLogger := logger.With(slog.String("operation", "process_order"))
    opLogger.Info("starting operation")

    return processOrder(opLogger)
}

// Data access layer - enrich further if needed
func processOrder(logger *slog.Logger) error {
    // Add data-specific fields
    dbLogger := logger.With(slog.String("database", "orders_db"))

    // All fields from the entire chain are included:
    // request_id, user_id, operation, and database
    dbLogger.Info("executing query")
    return nil
}

In this pattern:

  • Request boundary: Create an enriched logger with request-level fields
  • Business logic: Add operation-specific fields by calling .With() on the passed logger
  • Data layer: Add implementation details by calling .With() again
  • Final logs: All accumulated fields flow through the entire call stack
Storing Loggers in Context

When you need to store and retrieve a logger from context (e.g., in middleware), use WithLogger() and FromContext():

ctx := context.Background()

// Store the logger in context
ctx = log.WithLogger(ctx, logger)

// Retrieve it later (panics if not present)
logger := log.FromContext(ctx)

Performance

The v2 log package is a significant performance improvement over the v1 Logrus-based approach.

v2 vs v1 performance comparison

The migration from Logrus (v1) to slog (v2) provides substantial performance improvements across all logging scenarios:

Attributes Logrus v1 LabKit v2 Improvement
0 659.6 ns 486.6 ns 26% faster
1 992.0 ns 560.4 ns 43% faster
5 1574 ns 808.8 ns 49% faster
10 2539 ns 1250 ns 51% faster

Memory allocation is also significantly better:

Attributes Logrus v1 LabKit v2 Savings
0 872 B / 19 allocs 48 B / 3 allocs 94% less
5 2292 B / 35 allocs 289 B / 8 allocs 87% less
10 3367 B / 49 allocs 1140 B / 17 allocs 66% less
Real-world impact

For a service making 10,000 logs/second with average 3 attributes per log:

  • Logrus v1: 39.6 seconds of CPU per hour on logging
  • LabKit v2: 23.4 seconds per hour (41% reduction)

On an 8-core server, this saves approximately 3 minutes per hour of CPU time.

See the description of !427 for real-world impact analysis with concrete GitLab service examples.

Best practices

To get the best performance from the v2 logger:

  1. Create enriched loggers at request boundaries: When handling requests, create an enriched logger once with request-level fields and pass it through your call stack:

    // ✓ GOOD - created once per request
    requestLogger := logger.With(
        slog.String("request_id", reqID),
        slog.String("user_id", userID),
    )
    // Pass requestLogger through your handlers
    
  2. Reuse loggers for service-level attributes: When you have attributes that don't change (service name, environment), create a logger once at startup:

    // ✓ GOOD - created once at startup, reused for all requests
    serviceLogger := logger.With(
        slog.String("service", "my-service"),
        slog.String("env", os.Getenv("ENVIRONMENT")),
    )
    // Enrich further per-request
    requestLogger := serviceLogger.With(slog.String("request_id", reqID))
    
  3. Chain .With() calls for nested contexts: When adding fields at different layers, chain .With() calls on the logger passed to you:

    // Handler passes a logger
    // Business layer enriches it
    opLogger := logger.With(slog.String("operation", "checkout"))
    // Data layer enriches it further
    dbLogger := opLogger.With(slog.String("table", "orders"))
    // All fields accumulate in the final logger
    

Documentation

Overview

Package log provides a pre-configured slog.Logger that follows GitLab logging conventions: JSON output to stderr, UTC timestamps in RFC 3339 format, and structured logging with slog.

Quick start

logger := log.New()
logger.Info("server started", slog.String("addr", ":8080"))

New returns a logger with sensible defaults. Use NewWithConfig to customise the output writer, format, or log level:

logger := log.NewWithConfig(&log.Config{
	UseTextFormat: true,
})

Log level

The minimum log level is controlled by the GITLAB_LOG_LEVEL environment variable. Supported values are DEBUG, INFO, WARN, and ERROR (case-insensitive). When unset or invalid, defaults to INFO. Override programmatically via [Config.LogLevel].

Structured logging

Use slog.Logger's built-in [With] method to attach structured attributes to a logger. This is the canonical pattern for propagating request-scoped fields throughout a request lifecycle:

logger := log.New()
enriched := logger.With(
	slog.String(fields.CorrelationID, corrID),
	slog.String(fields.GitLabUserName, username),
)
enriched.Info("request completed")

For storing and retrieving loggers from context, use WithLogger and FromContext:

ctx = log.WithLogger(ctx, logger)
logger := log.FromContext(ctx)

File output

NewWithFile creates a logger that writes to a file. If the file cannot be opened, it gracefully degrades to stderr and returns an error, allowing the caller to decide how to proceed without blocking application startup.

Example

Example shows basic logger creation and usage. New() produces a JSON logger writing to stderr at INFO level. The log level can be overridden at runtime with the GITLAB_LOG_LEVEL environment variable.

package main

import (
	"log/slog"

	"gitlab.com/gitlab-org/labkit/v2/log"
)

func main() {
	logger := log.New()
	logger.Info("service started", slog.String("addr", ":8080"))
}

Index

Examples

Constants

View Source
const GitLabLogFormatEnvVar = "GITLAB_LOG_FORMAT"

GitLabLogFormatEnvVar is the environment variable used to control the log output format. Supported values are "json" (default) and "text".

View Source
const GitLabLogLevelEnvVar = "GITLAB_LOG_LEVEL"

GitLabLogLevelEnvVar is the environment variable used to control the minimum log level. Supported values are "DEBUG", "INFO", "WARN", and "ERROR" (case-insensitive). Defaults to "INFO" if unset or invalid.

View Source
const GitLabLogTimezoneEnvVar = "GITLAB_LOG_TIMEZONE"

GitLabLogTimezoneEnvVar is the environment variable used to control the timezone for log timestamps. Supported values are IANA timezone names (e.g., "UTC", "America/New_York", "Europe/London"). Defaults to "UTC" if unset or invalid.

Variables

This section is empty.

Functions

func ContentType

func ContentType(ct string) slog.Attr

ContentType returns an slog.Attr for the content_type field.

func CorrelationID

func CorrelationID(id string) slog.Attr

CorrelationID returns an slog.Attr for the correlation_id field.

func DurationS

func DurationS(d time.Duration) slog.Attr

DurationS returns an slog.Attr for the duration_s field. The duration is converted to seconds automatically.

func Error

func Error(err error) slog.Attr

Error returns an slog.Attr for the error_message field. The error message is evaluated lazily: err.Error() is only called if the log record is actually handled (i.e. the log level is enabled). This avoids unnecessary work when the log level is below the threshold. Use this in preference to ErrorMessage(err.Error()).

Example

ExampleError shows the lazy error helper. The error string is only evaluated if the log level is enabled, avoiding unnecessary allocation on hot paths.

package main

import (
	"context"
	"errors"
	"log/slog"

	"gitlab.com/gitlab-org/labkit/v2/log"
)

func main() {
	logger := log.New()
	err := errors.New("connection refused")
	logger.LogAttrs(context.Background(), slog.LevelError, "database unreachable",
		log.Error(err),
	)
}

func ErrorMessage

func ErrorMessage(msg string) slog.Attr

ErrorMessage returns an slog.Attr for the error_message field.

func ErrorType

func ErrorType(errType string) slog.Attr

ErrorType returns an slog.Attr for the error_type field.

func FromContext

func FromContext(ctx context.Context) *slog.Logger

FromContext retrieves a logger from context. Panics if no logger was stored.

func GitLabUserID

func GitLabUserID(id int) slog.Attr

GitLabUserID returns an slog.Attr for the gl_user_id field.

func GitLabUserName

func GitLabUserName(name string) slog.Attr

GitLabUserName returns an slog.Attr for the gl_user_name field.

func HTTPHost

func HTTPHost(host string) slog.Attr

HTTPHost returns an slog.Attr for the host field.

func HTTPMethod

func HTTPMethod(method string) slog.Attr

HTTPMethod returns an slog.Attr for the method field.

func HTTPProto

func HTTPProto(proto string) slog.Attr

HTTPProto returns an slog.Attr for the proto field.

func HTTPReferrer

func HTTPReferrer(referrer string) slog.Attr

HTTPReferrer returns an slog.Attr for the referrer field. The referrer should have sensitive query parameters masked before passing.

func HTTPStatusCode

func HTTPStatusCode(code int) slog.Attr

HTTPStatusCode returns an slog.Attr for the status field.

func HTTPURI

func HTTPURI(uri string) slog.Attr

HTTPURI returns an slog.Attr for the uri field. The URI should have sensitive query parameters masked before passing.

func HTTPURL

func HTTPURL(url string) slog.Attr

HTTPURL returns an slog.Attr for the url field.

func HTTPUserAgent

func HTTPUserAgent(ua string) slog.Attr

HTTPUserAgent returns an slog.Attr for the user_agent field.

func New

func New() *slog.Logger

New - a handy wrapper that configures the slog.Logger in a consistent fashion. Engineers should always default to using this constructor to ensure that they can take advantage of future global enhancements to our logging setup.

func NewWithConfig

func NewWithConfig(cfg *Config) *slog.Logger

NewWithConfig - a constructor that allows you to overwrite some of the core constructs within the Logger for your own nefarious purposes.

Example

ExampleNewWithConfig shows how to create a human-readable text logger at DEBUG level — useful during local development.

package main

import (
	"log/slog"

	"gitlab.com/gitlab-org/labkit/v2/log"
)

func main() {
	level := slog.LevelDebug
	logger := log.NewWithConfig(&log.Config{
		UseTextFormat: true,
		LogLevel:      &level,
	})
	logger.Debug("debug logging enabled")
	_ = logger
}

func NewWithFile

func NewWithFile(filePath string, cfg *Config) (*slog.Logger, io.Closer, error)

NewWithFile creates a logger writing to a file. Caller is responsible for closing the returned file. Should there be an error with the opening of the filePath this will gracefully degrade and provide a Stderr logger alongside an error. This allows the consumer to decide how they wish to proceed rather than outright blocking application startup.

func RemoteAddr

func RemoteAddr(addr string) slog.Attr

RemoteAddr returns an slog.Attr for the remote_addr field.

func RemoteIP

func RemoteIP(ip string) slog.Attr

RemoteIP returns an slog.Attr for the remote_ip field.

func TCPAddress

func TCPAddress(addr string) slog.Attr

TCPAddress returns an slog.Attr for the tcp_address field.

func TTFBS

func TTFBS(d time.Duration) slog.Attr

TTFBS returns an slog.Attr for the ttfb_s field. The duration is converted to seconds automatically.

func WithLogger

func WithLogger(ctx context.Context, logger *slog.Logger) context.Context

WithLogger stores a logger in context.

Example

ExampleWithLogger demonstrates the canonical logging pattern: create a logger with request-scoped fields once and reuse it for all log lines, without repeating them at each call site.

package main

import (
	"log/slog"

	"gitlab.com/gitlab-org/labkit/v2/log"
)

func main() {
	logger := log.New()

	// Create an enriched logger with request-scoped fields
	enriched := logger.With(
		log.CorrelationID("req-abc-123"),
		log.GitLabUserID(42),
	)

	// correlation_id and gl_user_id are included on every line from this logger
	enriched.Info("project created", slog.Int("project_id", 99))
	enriched.Info("webhook dispatched", slog.Int("project_id", 99))
}

func WrittenBytes

func WrittenBytes(n int64) slog.Attr

WrittenBytes returns an slog.Attr for the written_bytes field.

Types

type Config

type Config struct {
	// Writer - a lower level construct that allows
	// engineers to have finer-grained control over
	// how and where logs are written to file.
	Writer io.Writer

	// UseTextFormat - set this to true if you require
	// text formatted logs.
	UseTextFormat bool

	// Clock - allows the consumer to provide their own
	// TimeFunc that is used to provide the timestamp when
	// emitting logs.
	// The logger defaults to UTC to try and ensure
	// consistency across our services.
	Clock TimeFunc

	// Location - the timezone location for log timestamps.
	// Defaults to UTC. Can be overridden by passing a specific location
	// or via the GITLAB_LOG_TIMEZONE environment variable.
	// When nil, the location from the environment variable is used,
	// falling back to UTC if unset or invalid.
	Location *time.Location

	// LogLevel - represents the minimum log level that should be output by
	// the logger. When nil, the value of GITLAB_LOG_LEVEL is used, falling
	// back to slog.LevelInfo if the environment variable is unset or invalid.
	// Set this explicitly to override the environment variable.
	LogLevel *slog.Level
}

Config holds the configuration for creating a new logger.

type TimeFunc

type TimeFunc func() time.Time

TimeFunc - exclusively used for testing purposes please do not use in production.

Jump to

Keyboard shortcuts

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