deepstack

package module
v0.1.4 Latest Latest
Warning

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

Go to latest
Published: Dec 26, 2025 License: 0BSD Imports: 13 Imported by: 8

README

DeepStack

Overview

DeepStack is a structured logging and error management library for the Ocelot Ecosystem based on the Go SDK library log/slog.

Error Design

Here is the DeepStack error data structure:

type DeepStackError struct {
    message      string
    stackTrace   string
    context      map[string]interface{}
}
  • Log Or Return Principle: To avoid duplication, either log or return an error, but not both. Therefore, errors are created in a low-level function and passed up to a higher-level function, where they are logged once. However, when an error is logged in a high-level function, it must contain information about which function caused it.
  • Therefore, a stack trace is included in the DeepStack error upon creation.
  • DeepStack error implements Go error interface, allowing it to be used seamlessly with Go's error handling mechanisms and reducing its coupling with the code in which it is used.
  • DeepStackError Structure: Other logging libraries often encode context and stack trace information in a single error string, adding encoding complexity. By contrast, DeepStack errors are rich data structures that contain additional fields for context and stack traces. This makes them easy to understand and avoids unnecessary complexity.
  • Adding Error Context: DeepStack error data structures have a context field that can store key-value pairs. These pairs can be added to extend the context during DeepStack error creation or by intermediate functions passing up the DeepStack error. As these operations are performed directly on the error data structure, the process is much lighter than the costly encoding operations performed by other logging libraries.
Logging Design
  • Structured Logging is the general use case of the DeepStack library that allows for easy filtering and searching of logs.
  • Error Logging is a special case in which the DeepStack logger reflects on the error type. If it is a DeepStack error, the library prints all of this information to the console and the log file in a readable manner. This can be extended later to send logs to a server.

Usage Overview

Basic Structured Logging
func main() {
    logger := deepstack.NewDeepStackLogger(NewRawConsoleHandler(slog.LevelInfo))
    // The design of the usage aimed for minimal overhead and simplicity, ideally with one-liners.
    logger.Info("user logged in", "name", "john", "age", 23)
}

Output:

2025-08-31 16:58:52.135 INFO main.go:10 "user logged in" age=23 name=john
Error Management

Use a simple create → propagate → handle lifecycle with DeepStack. Stack traces are captured once at creation. Logging happens once at a boundary, e.g. an HTTP handler.

1) Error creation

Create a DeepStack error at the first failure point so the stack trace points to the origin.

  • New failure: deepstack.NewError("resource not found")
  • From a Go error: deepstack.NewError(err.Error())
2) Error propagation

Return the error upward. Intermediate layers do not log, but they may enrich the error with additional context via AddContext() and return it.

3) Error handling

Log the error at the boundary, by passing the error via deepstack.ErrorField to the logger. If a non-DeepStack error is logged this way, the logger emits a warning to help you find places where wrapping to DeepStack errors was missed.

Context

At any stage you can add context to a DeepStack error as key–value pairs. This context travels with the error and is included when it’s finally logged.

Full Logging Example
package main

import (
    "os/exec"
	"log/slog"
    "github.com/ocelot-cloud/deepstack"
)

var logger = deepstack.NewDeepStackLogger(slog.LevelInfo)

const (
    // good practice to hard code field names for reusability
    AccessDeniedField = "access_denied"
)

func main() {
    err := func1()
    logger.Error("resource access operation failed", deepstack.ErrorField, err)
}

// intermediate function passing up the error
func func1() error {
    err := func2()
    if err != nil {
        return err
    }
    // do some other stuff here
    return nil
}

// intermediate function adding context and then passing up the error
func func2() error {
    return logger.AddContext(func3(), AccessDeniedField, "access token not found")
}

var someCondition = true

// the function where the error occurs the first time
func func3() error {
    if someCondition {
        // create own error
        return logger.NewError("access token not found", AccessDeniedField, "no access token provided")
    } else {
        // wrap error from external library
        err := exec.Command("not-existing-command").Run()
        return logger.NewError(err.Error())
    }
}

Output:

main.func3
    /home/user/GolandProjects/playground/main.go:42
main.func2
    /home/user/GolandProjects/playground/main.go:33
main.func1
    /home/user/GolandProjects/playground/main.go:23
main.main
    /home/user/GolandProjects/playground/main.go:17
runtime.main
    /home/user/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.4.linux-amd64/src/runtime/proc.go:283
runtime.goexit
    /home/user/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.4.linux-amd64/src/runtime/asm_amd64.s:1700

2025-08-31 16:53:38.022 ERROR main.go:18 "resource access operation failed" error_cause="access token not found" access_denied="access token not found"
Asserting DeepStack Errors in Tests

Use the provided assertion helper to verify DeepStack errors in tests. It requires an exact match of the error message and the context.

func TestStuff(t *testing.T) {
    err := someFunction()
    deepstack.AssertDeepStackError(t, err, "some error message", "name", "john", "age", "23")
}
Handlers

A common pattern is for an HTTP handler to receive a request and pass its data to the business logic, which may return an error. The handler is typically responsible for logging the error, mapping it to the relevant HTTP status code and generating an appropriate message for the client.

In standard Go, the business logic returns typed errors and the handlers use type switches to determine the response. However, DeepStack errors use a single error type, which renders this approach incompatible.

Instead, we define a set of error messages that can safely be exposed to clients. If the error matches one of these strings, it is returned unchanged. Otherwise, a default message is used to ensure that no sensitive error messages are returned. For example:

var (
	expectedErrors  = MapOf("service not configured", "operation not allowed"
    Logger          = deepstack.NewDeepStackLogger(slog.LevelInfo)
)

func SomeHandler(w http.ResponseWriter, r *http.Request) {
    if err := BusinessLogicServer.DoStuff(); err != nil {
        WriteResponseError(w, expectedErrors, err)
        return
    }
}

func MapOf(expectedErrors ...string) map[string]any {
    var errorMap = map[string]any{}
    for _, expectedError := range expectedErrors {
        errorMap[expectedError] = nil
    }
    return errorMap
}

func WriteResponseError(w http.ResponseWriter, expectedErrors map[string]any, err error, context ...any) {
    if expectedErrors == nil {
        expectedErrors = map[string]any{}
    }

    var actualErrorMessage string
    deepStackError, isDeepStackError := err.(*deepstack.DeepStackError)
    if isDeepStackError {
        actualErrorMessage = deepStackError.Message
    } else {
        Logger.Warn("expected a DeepStack error, but got a regular error")
        actualErrorMessage = err.Error()
    }

    _, isExpectedError := expectedErrors[actualErrorMessage]

    if isExpectedError {
        // for expected errors, we don't need a stack trace or context of a DeepStackError in the logs
        Logger.Info(actualErrorMessage, context)
        http.Error(w, actualErrorMessage, http.StatusBadRequest)
    } else {
        if isDeepStackError {
            Logger.Error(actualErrorMessage, deepstack.ErrorField, deepStackError, context)
        } else {
            Logger.Error(actualErrorMessage, context)
        }
        http.Error(w, OperationFailedError, http.StatusBadRequest)
    }
}
Register New Log Handlers

The logger created by NewDeepStackLogger() comes with a console handler by default. Additional log handlers can be registered there to perform actions such as writing logs to files or sending them to a logging server.

Contributing

Please read the Community articles for more information on how to contribute to the project.

License

This project is licensed under a permissive open source license, the 0BSD License. See the LICENSE file for details.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AssertDeepStackError added in v0.0.7

func AssertDeepStackError(t *testing.T, err error, expectedMessage string, expectedContext ...any)

func NewJsonConsoleHandler added in v0.1.1

func NewJsonConsoleHandler(level slog.Level) *slog.JSONHandler

Types

type ConsoleHandler added in v0.0.4

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

func NewRawConsoleHandler added in v0.1.1

func NewRawConsoleHandler(level slog.Level) *ConsoleHandler

func (ConsoleHandler) Enabled added in v0.0.4

func (s ConsoleHandler) Enabled(_ context.Context, lvl slog.Level) bool

func (ConsoleHandler) Handle added in v0.0.4

func (s ConsoleHandler) Handle(_ context.Context, r slog.Record) error

func (ConsoleHandler) WithAttrs added in v0.0.4

func (s ConsoleHandler) WithAttrs(a []slog.Attr) slog.Handler

func (ConsoleHandler) WithGroup added in v0.0.4

func (s ConsoleHandler) WithGroup(string) slog.Handler

type DeepStackError

type DeepStackError struct {
	Message    string
	StackTrace string
	Context    map[string]any
}

func (*DeepStackError) Error

func (d *DeepStackError) Error() string

Error returns all fields, so the logs clearly show when an error has been wrapped incorrectly multiple times by different layers.

type DeepStackLogger

type DeepStackLogger interface {
	Debug(msg any, context ...any)
	Info(msg any, context ...any)
	Warn(msg any, context ...any)
	Error(msg any, context ...any)
	NewError(msg string, context ...any) error
	AddContext(err error, context ...any) error
}

func NewDeepStackLogger

func NewDeepStackLogger(additionalHandlers ...slog.Handler) DeepStackLogger

type DeepStackLoggerImpl

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

func (*DeepStackLoggerImpl) AddContext added in v0.0.3

func (m *DeepStackLoggerImpl) AddContext(err error, context ...any) error

func (*DeepStackLoggerImpl) Debug

func (m *DeepStackLoggerImpl) Debug(msgOrErr any, context ...any)

func (*DeepStackLoggerImpl) Error

func (m *DeepStackLoggerImpl) Error(msgOrErr any, context ...any)

func (*DeepStackLoggerImpl) Info

func (m *DeepStackLoggerImpl) Info(msgOrErr any, context ...any)

func (*DeepStackLoggerImpl) NewError

func (m *DeepStackLoggerImpl) NewError(msg string, context ...any) error

func (*DeepStackLoggerImpl) Warn

func (m *DeepStackLoggerImpl) Warn(msgOrErr any, context ...any)

type LoggingBackend

type LoggingBackend interface {
	ShouldLogBeSkipped(level slog.Level) bool
	LogRecord(logRecord *Record)
	PrintStackTrace(message string)
	LogWarning(message string, kv ...any)
}

type LoggingBackendImpl

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

TODO add tests, maybe also dependencies, hide slog dependency somehow?

func (*LoggingBackendImpl) LogRecord added in v0.0.4

func (s *LoggingBackendImpl) LogRecord(logRecord *Record)

func (*LoggingBackendImpl) LogWarning

func (s *LoggingBackendImpl) LogWarning(message string, kv ...any)

func (*LoggingBackendImpl) PrintStackTrace added in v0.0.4

func (s *LoggingBackendImpl) PrintStackTrace(stackTrace string)

func (*LoggingBackendImpl) ShouldLogBeSkipped

func (s *LoggingBackendImpl) ShouldLogBeSkipped(level slog.Level) bool

type Record added in v0.0.4

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

func (*Record) AddAttrs added in v0.0.4

func (r *Record) AddAttrs(key string, value any)

type StackTracer added in v0.0.4

type StackTracer interface {
	GetStackTrace() string
}

type StackTracerImpl added in v0.0.4

type StackTracerImpl struct{}

func (*StackTracerImpl) GetStackTrace added in v0.0.4

func (s *StackTracerImpl) GetStackTrace() string

Jump to

Keyboard shortcuts

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