Skip to content

Instantly share code, notes, and snippets.

@quad
Last active April 23, 2024 13:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save quad/56c924d655362b2f5c1c70e1c52d805e to your computer and use it in GitHub Desktop.
Save quad/56c924d655362b2f5c1c70e1c52d805e to your computer and use it in GitHub Desktop.
Structured logging deprecates log levels [DRAFT]

Structured logging deprecates log levels

I read an argument for only two log levels: INFO and ERROR. I responded essentially saying:

  1. I lightly agree that we could get away with two log levels.
  2. I strongly disagree that we only need two ways to log.

Like the blog post's author, most of systems that I've worked on have had poor logging practices. But unlike the blog post's author, I strongly suspect that's because most logging APIs offer poor affordances. That is to say, their design makes it easy to do the wrong thing.

What follows will be a thought experiment for an alternative logging API design that, in an imaginary world, would be harder to misuse.

But, first, let's review what I mean by "most logging APIs."

Most Logging APIs

log4j (Java)

https://logging.apache.org/log4j/2.x/javadoc/log4j-api/org/apache/logging/log4j/Logger.html

logging (Python)

https://docs.python.org/3/howto/logging.html

tracing (Rust)

https://docs.rs/tracing/latest/tracing/index.html

log (Go)

https://pkg.go.dev/log https://pkg.go.dev/log/slog

Flaws of most logging APIs

  • Too many levels, no guidance on when to use them.
  • Limit support for structured logging (contexts, spans, events)
    • Logs aren't events
  • Hard (or impossible) to compose the conditions for when to log
    • log4j and friends do this in global filters; IMHO policy is easier to maintain and less magic when located close to the mechanism

An imaginary alternative logging API

# Structured logging

logger = Logger.new()
  .record(key=value)
  .record(log.tags.ERROR=true)

# Good affordances

def Logger.verbose(self):
  return self.record(log.tags.VERBOSE=True)

def Logger.sampled(self):
  return self.record(
    log.tags.SAMPLE_KEY=key(prefix=caller),
    log.tags.SAMPLE_RATE=0.1,
  )

def Logger.error(self, error):
  return self.record(
    log.tags.ERROR=True,
    log.tags.ERROR_DETAIL=error,
  )

def Logger.fatal(self):
  return self.record(log.tags.FATAL=True)

# Example uses

logger = Logger.new()

logger.log("A normal log")
logger.verbose().log("way too much detail for normal purposes")
logger.verbose().error(e).log("handled an error")

with logger.sampled() as logger:
  logger.log("got a request on a high QPS endpoint")
  
  try:
    something()
  except e:
    logger.error(e).log("could not handle this error")
    raise e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment