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