Skip to content

Design a consistent Logging API for all language runtimes

One of the key outcomes from the Labkit v2 Logging SDK is that there is a common, consistent API across all of the various runtimes we'll be supporting when it comes to the task of logging.

This issue attempts to capture a cohesive API in semi-pseudocode form so that it's easier for us to compare between the language runtimes.

Go

import glog "gitlab.com/gitlab-org/labkit/v2/log"

// let's start with how we foresee the most common use case
glog.Info(ctx, "gitaly:someFunc - something nice happened")
// {"level": "info", "msg": "gitaly:someFunc - something nice happened"}
glog.Error(ctx, "gitaly:someFunc - oh my goodness, the world is ending")
// {"level": "error", "msg": "gitaly:someFunc - oh my goodness, the world is ending"}
glog.Warn(ctx, "gitaly:someFunc - I promise I'm not picking on Gitaly")
// {"level": "warn", "msg": "gitaly:someFunc - I promise I'm not picking on Gitaly"}
// how do we enrich these to contain pieces of information we care about?
ctx = log.WithFields(ctx, glog.Fields{
    "trace_id": "my-super-trace-id",
})
// contexts in Go typically get passed along the lifecycle of a request
// through the system. This allows us to then easily correlate subsequent
// log information.

glog.Info(ctx, "gitaly:someFunc - something nice happened")
// {"level": "info", "msg": "gitaly:someFunc - something nice happened", "trace_id": "my-super-trace-id"}
// one off enrichment field enrichment can happen like so:
glog.Info(ctx, "gitaly:someFunc - something nice happened", glog.Fields{"some-field": "hello"})

Addendum - the log.Fields map could be tightly constrained in the future to a map with pre-determined keys. This could help us avoid subsequent observability field explosion issues which plague the dedicated team.

Ruby

rubyrequire 'labkit/v2/log'

# Most common use case - simple logging
Labkit::Log.info("gitaly:someFunc - something nice happened")
# {"level": "info", "msg": "gitaly:someFunc - something nice happened"}

Labkit::Log.error("gitaly:someFunc - oh my goodness, the world is ending")
# {"level": "error", "msg": "gitaly:someFunc - oh my goodness, the world is ending"}

Labkit::Log.warn("gitaly:someFunc - I promise I'm not picking on Gitaly")
# {"level": "warn", "msg": "gitaly:someFunc - I promise I'm not picking on Gitaly"}

# Enriching logs with contextual information
# Ruby doesn't have Go's context pattern, so we'd use thread-local storage
# or pass context explicitly

Labkit::Log.with_fields(trace_id: "my-super-trace-id") do
  Labkit::Log.info("gitaly:someFunc - something nice happened")
  # {"level": "info", "msg": "gitaly:someFunc - something nice happened", "trace_id": "my-super-trace-id"}
end

# Alternative approach using explicit context passing
logger = Labkit::Log.with_fields(trace_id: "my-super-trace-id")
logger.info("gitaly:someFunc - something nice happened")

# One-off field enrichment
Labkit::Log.info("gitaly:someFunc - something nice happened", "some-field": "hello")
# {"level": "info", "msg": "gitaly:someFunc - something nice happened", "some-field": "hello"}

# Or combining with existing context
Labkit::Log.with_fields(trace_id: "my-super-trace-id") do
  Labkit::Log.info("gitaly:someFunc - something nice happened", user_id: 123)
  # {"level": "info", "msg": "gitaly:someFunc - something nice happened", "trace_id": "my-super-trace-id", "user_id": 123}
end

Python

import labkit.v2.log as log

# Most common use case - simple logging
log.info("gitaly:someFunc - something nice happened")
# {"level": "info", "msg": "gitaly:someFunc - something nice happened"}

log.error("gitaly:someFunc - oh my goodness, the world is ending")
# {"level": "error", "msg": "gitaly:someFunc - oh my goodness, the world is ending"}

log.warn("gitaly:someFunc - I promise I'm not picking on Gitaly")
# {"level": "warn", "msg": "gitaly:someFunc - I promise I'm not picking on Gitaly"}

# Enriching logs with contextual information
# Python can use context managers or contextvars for request-scoped data

with log.with_fields(trace_id="my-super-trace-id"):
    log.info("gitaly:someFunc - something nice happened")
    # {"level": "info", "msg": "gitaly:someFunc - something nice happened", "trace_id": "my-super-trace-id"}

# Alternative approach using explicit logger instance
logger = log.with_fields(trace_id="my-super-trace-id")
logger.info("gitaly:someFunc - something nice happened")

# One-off field enrichment using keyword arguments
log.info("gitaly:someFunc - something nice happened", some_field="hello")
# {"level": "info", "msg": "gitaly:someFunc - something nice happened", "some_field": "hello"}

# Or using a fields dictionary (Go-style)
log.info("gitaly:someFunc - something nice happened", fields={"some_field": "hello"})

# Combining with existing context
with log.with_fields(trace_id="my-super-trace-id"):
    log.info("gitaly:someFunc - something nice happened", user_id=123)
    # {"level": "info", "msg": "gitaly:someFunc - something nice happened", "trace_id": "my-super-trace-id", "user_id": 123}

# Async-aware context (for async applications)
import asyncio

async def some_handler():
    async with log.with_fields_async(trace_id="my-super-trace-id"):
        log.info("gitaly:someFunc - async context preserved")
Edited by Elliot Forbes