Skip to content

Repository files navigation

LogEvent

CI Go Reference

This library provides utilities to implement the concept of emitting one canonical log (wide log event) after processing a unit of work, inspired by logging patterns from companies like Stripe or Google.

This library provides the raw functionality to implement canonical logging for any unit of work, and also provides two middlewares (one for HTTP and another for gRPC) to be used out of the box. Check the examples folder for more information.

The steps are the following:

  • We define a struct that we are going to update/populate when serving a request.
  • We implement the Log method of the LogEvent interface. This allows us to change the way we want to log the event based on the values.
  • When serving the unit of work, we populate that struct event with all the useful information that we want to see in the log entry.
  • Once the unit of work is served, the library will log that canonical log event by calling the method Log we implemented.

This is better described in loggingsucks.

To see it directly in action, check the examples folder.

Requirements

  • Go 1.25.0 or newer

⬇️ How to get it

go get github.com/manuelarte/logevent

🚀 Features

The library provides generic functions that can be used to implement the concept of adding a LogEvent to a context.Context, performing some work while updating the log event, and then calling Log on that LogEvent.

It also provides out-of-the-box implementations for:

Canonical Logging (without middleware)

The library provides primitives to implement canonical logging without needing middleware. This is useful for background jobs, service layers, or any scenario where you want to manually control the log event lifecycle.

package main

import (
 "context"
 "log/slog"

 "github.com/manuelarte/logevent"
)

type taskLogEvent struct {
 taskID  string
 status  string
 elapsed int64
}

func (e taskLogEvent) Log(ctx context.Context, li *slog.Logger) {
 li.InfoContext(ctx, "Task completed", slog.String("task_id", e.taskID), slog.String("status", e.status))
}

func processTask(ctx context.Context, taskID string, logger *slog.Logger) error {
 // Step 1. Add the log event to the context
 ctx, logItFunc := logevent.AddLogEventToContext[*slog.Logger](ctx, taskLogEvent{taskID: taskID})
 // Step 2. Get the defer function that will log the event
 defer logItFunc(logger)

 // Do some work...

 // Step 3. Update the log event during processing
 _ = logevent.UpdateLogEvent(ctx, func(e *taskLogEvent) {
  e.elapsed = elapsedTime
  e.status = "completed"
 })

 // The log event is automatically logged when defer is called
 return nil
}

HTTP Middleware

This library provides a middleware that can be used to emit a log event after an HTTP request.

package main

import (
    "context"
    "log/slog"
    "net/http"

    "github.com/manuelarte/logevent"
    logeventhttp "github.com/manuelarte/logevent/mw/http"
)

// Step 1. Define your log event struct and how to log it.
type transferLogEvent struct {
    source   string
    target   string
    amount   string
    transferErr error
}

// Log the event either with Info if everything succeeded or with Error if there was an error.
func (e transferLogEvent) Log(ctx context.Context, li *slog.Logger) {
    if e.transferErr != nil {
        li.ErrorContext(
            ctx,
           "Error when transferring money",
           slog.String("source", e.source),
           slog.String("target", e.target),
           slog.String("amount", e.amount),
           slog.Any("error", e.transferErr),
        )
        return
    }

     li.InfoContext(
          ctx,
          "Money transferred successfully",
          slog.String("source", e.source),
          slog.String("target", e.target),
          slog.String("amount", e.amount),
     )
}

// Step 2. Add the middleware to your endpoint.
func registerRoutes() {
     http.Handle(
          "/my-endpoint",
          logeventhttp.AddLogEventMiddleware(transferLogEvent{}, slog.Default())(http.HandlerFunc(myHandler)),
     )
}

func myHandler(w http.ResponseWriter, r *http.Request) {
     // Step 3. Update your log event while serving the request.
     _ = logevent.UpdateLogEvent(r.Context(), func(t *transferLogEvent) {
          t.source = "Alice"
          t.target = "Bob"
          t.amount = "100"
     })
     // ...
     err := transferMoney("Alice", "Bob", 100)
     _ = logevent.UpdateLogEvent(r.Context(), func(t *transferLogEvent) {
        t.transferErr = err
     })
     // ...
}

gRPC Interceptor

This library also provides a unary server interceptor for your gRPC server.

package main

import (
     "context"
     "log/slog"

     "google.golang.org/grpc"

     "github.com/manuelarte/logevent"
     logeventgrpc "github.com/manuelarte/logevent/mw/grpc"
)

// Step 1. Define your log event struct and how to log it.
type transferLogEvent struct {
     source string
     target string
     amount string
     transferErr    error
}

// Log the event either with Info if everything succeeded or with Error if there was an error.
func (e transferLogEvent) Log(ctx context.Context, li *slog.Logger) {
     if e.transferErr != nil {
        li.ErrorContext(
           ctx,
           "Error when transferring money",
           slog.String("source", e.source),
           slog.String("target", e.target),
           slog.String("amount", e.amount),
           slog.Any("error", e.transferErr),
    )
    return
 }

    li.InfoContext(
          ctx,
          "Money transferred successfully",
          slog.String("source", e.source),
          slog.String("target", e.target),
          slog.String("amount", e.amount),
    )
}

// Step 2. Add the interceptor to your server.
server := grpc.NewServer(
    grpc.UnaryInterceptor(
        logeventgrpc.UnaryServerInterceptor(transferLogEvent{}, slog.Default()),
    ),
)

func (s transferMoneyServer) Transfer(ctx context.Context, req *TransferMoneyRequest) (*TransferMoneyResponse, error) {
    // Step 3. Update your log event while handling the request.
    _ = logevent.UpdateLogEvent(ctx, func(t *transferLogEvent) {
          t.source = "Alice"
          t.target = "Bob"
          t.amount = "100"
    })
    // ...
    err := transferMoney("Alice", "Bob", 100)
    _ = logevent.UpdateLogEvent(ctx, func(t *transferLogEvent) {
        t.transferErr = err
    })
    // ...
}

Architecture

This library provides an HTTP middleware and a gRPC interceptor, but also a generic implementation for a custom way to serve a request that encapsulates:

  1. Creating a per-request copy of the log event struct
  2. Wrapping it with thread-safe access (concurrency support)
  3. Storing it in the request context
  4. Deferring the log output until after the request handler completes
  5. Checking for any updates made by the handler

This ensures consistent behavior and makes it easy to update the logging logic in a single place.

By having custom log events structs, you can make more complex logging decisions based on the values of the log event, and also make it easier to add new fields to the log event without changing the logging logic.

Some examples can be:

  • log only a percentage of the successful requests:
func (e transferLogEvent) Log(ctx context.Context, li *slog.Logger) {
    if e.transferErr != nil {
        // Log the error
    }
 if rand.Float64() < 0.1 { // log only 10% of the successful requests
        // Log the successful request
    }
}
  • log only if the elapsed time is greater than a certain amount of time:
func (e transferLogEvent) Log(ctx context.Context, li *slog.Logger) {
    if e.transferErr != nil {
        // Log the error
    }
    if e.elapsed > 1000 { // log only if the elapsed time is greater than 1000ms
        // Log the successful request
    }
}

Examples

For runnable examples check the examples folder:

About

Middlewares to emit a canonical log event (wide log event) after processing a unit of work. This library provides an HTTP middleware and a gRPC interceptor.

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages