deepstack

package module
v0.1.8 Latest Latest
Warning

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

Go to latest
Published: May 1, 2026 License: 0BSD Imports: 13 Imported by: 3

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")
}
Register New Log Handlers

Log handlers can be registered via NewDeepStackLogger() to write logs to console or files, or sending them to a log database. This package provides a default console handler, but you can integrate your own implementations as well.

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

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

func NewJsonConsoleHandler

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

Types

type ConsoleHandler

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

func NewRawConsoleHandler

func NewRawConsoleHandler(level slog.Level) *ConsoleHandler

func (ConsoleHandler) Enabled

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

func (ConsoleHandler) Handle

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

func (ConsoleHandler) WithAttrs

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

func (ConsoleHandler) WithGroup

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

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

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

func (*LoggingBackendImpl) LogWarning

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

func (*LoggingBackendImpl) PrintStackTrace

func (s *LoggingBackendImpl) PrintStackTrace(stackTrace string)

func (*LoggingBackendImpl) ShouldLogBeSkipped

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

type Record

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

func (*Record) AddAttrs

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

type StackTracer

type StackTracer interface {
	GetStackTrace() string
}

type StackTracerImpl

type StackTracerImpl struct{}

func (*StackTracerImpl) GetStackTrace

func (s *StackTracerImpl) GetStackTrace() string

Jump to

Keyboard shortcuts

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