logevent

package module
v0.0.6 Latest Latest
Warning

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

Go to latest
Published: Aug 27, 2026 License: MIT Imports: 5 Imported by: 0

README

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)

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

	// Do some work...

	// Step 4. Update the log event with final status
	_ = logevent.UpdateLogEvent(ctx, func(e *taskLogEvent) {
		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
    Err    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.Err != 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.Err),
        )
        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.Err = 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
     Err    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.Err != 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.Err),
    )
    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.Err = 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.

Examples

For runnable examples check the examples folder:

Documentation

Overview

Package logevent provides utilities to implement canonical logging (wide log events) in Go.

Canonical logging emits a single structured log entry at the end of a unit of work (e.g., HTTP request, gRPC call, or background task) capturing the full lifecycle context, metadata, and status.

The package provides core context helpers (AddLogEventToContext, UpdateLogEvent, [LogItFunc], HandleWithLogEvent) as well as ready-to-use middleware for HTTP (in subpackage mw/http) and gRPC (in subpackage mw/grpc).

Index

Constants

This section is empty.

Variables

View Source
var ErrLogEventNotInitialized = errors.New("LogEvent not initialized")

ErrLogEventNotInitialized error returned when adding context to a log event but the log event was not initialized.

Functions

func AddLogEventToContext added in v0.0.6

func AddLogEventToContext[L Logger, T any, PT PtrLogEvent[L, T]](
	parent context.Context,
	t T,
) (context.Context, func(l L))

AddLogEventToContext adds a log event to the context. It can be used to custom add a log event to the context in any kind of scenario.

The function performs the following steps:

  1. Type-asserts the provided log event to get the pointer type (required by the constraint)
  2. Creates a wrapper around the pointer for concurrency support (sync.Once, sync.RWMutex)
  3. Stores the wrapper in the context under a type-safe key

This design allows handlers to update the log event during request processing and ensures the log event is only logged once and is thread-safe. To add more context to the log event, use UpdateLogEvent.

func HandleWithLogEvent added in v0.0.6

func HandleWithLogEvent[L Logger, T any, PT PtrLogEvent[L, T]](
	ctx context.Context,
	t T,
	logger L,
	handler func(context.Context),
)

HandleWithLogEvent is a generic helper function that encapsulates the common pattern of adding a log event to the context and executing a handler. It is used by both the HTTP middleware and gRPC interceptor.

The function performs the following steps:

  1. Creates a per-request copy of the log event struct (to avoid concurrent modifications)
  2. Type-asserts the copy to get the pointer type (required by the constraint)
  3. Creates a wrapper around the pointer for concurrency support (sync.Once, sync.RWMutex)
  4. Stores the wrapper in the context under a type-safe key
  5. Defers a call to log the event after the handler completes
  6. Calls the provided handler function with the updated context
  7. Checks if the handler updated the log event in the context and uses the updated version

This design allows handlers to update the log event during request processing and ensures the log event is only logged once and is thread-safe.

func UpdateLogEvent added in v0.0.6

func UpdateLogEvent[L Logger, T any, PT PtrLogEvent[L, T]](ctx context.Context, f func(t PT)) error

UpdateLogEvent updates the log event stored in the context during request processing. It works with HTTP middleware, gRPC interceptors, or manual log event context lifecycle, allowing handlers to modify the log event that will be logged after the unit of work completes.

Parameters:

  • ctx: The context containing the log event.
  • f: A function that receives the pointer to the log event struct and modifies it.

Returns an error if the log event was not initialized (i.e., the request was not wrapped with AddLogEventMiddleware, UnaryServerInterceptor, or AddLogEventToContext).

Example with HTTP:

func myHandler(w http.ResponseWriter, r *http.Request) {
	_ = logevent.UpdateLogEvent(r.Context(), func(log *RequestLog) {
		log.Path = r.URL.Path
		log.Method = r.Method
	})
}

Example with gRPC:

func (s *server) MyRPC(ctx context.Context, req *pb.Request) (*pb.Response, error) {
	_ = logevent.UpdateLogEvent(ctx, func(log *RPCLog) {
		log.Method = "MyRPC"
	})
	return &pb.Response{}, nil
}

Types

type DifferentLogEventTypeError

type DifferentLogEventTypeError struct {
	// contains filtered or unexported fields
}

DifferentLogEventTypeError is returned when the log event type is different from the previous one.

func (DifferentLogEventTypeError) Error

Error implements the error interface.

type LogEvent

type LogEvent[L Logger] interface {
	// Log the event.
	Log(ctx context.Context, li L)
}

LogEvent is the interface that wraps how to Log the event.

type Logger added in v0.0.4

type Logger any

Logger is the interface that represents a logger.

type PtrLogEvent added in v0.0.6

type PtrLogEvent[L Logger, T any] interface {
	*T
	LogEvent[L]
}

PtrLogEvent helps to make that only a pointer can be passed to the middleware. It is a constraint that ensures PT is a pointer to type T and implements logevent.LogEvent.

Directories

Path Synopsis
mw
grpc
Package grpc provides gRPC interceptors for logevent.
Package grpc provides gRPC interceptors for logevent.
http
Package http provides HTTP middleware for the logevent package.
Package http provides HTTP middleware for the logevent package.

Jump to

Keyboard shortcuts

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