Documentation Index
Fetch the complete documentation index at: https://mintlify.com/magooney-loon/pb-ext/llms.txt
Use this file to discover all available pages before exploring further.
pb-ext provides a comprehensive logging system built on PocketBase’s logger with structured field support, request tracing, automatic error handling, and panic recovery.
Key Features
- Structured logging — key-value pairs for easy log parsing
- Request tracing — unique trace IDs for request correlation
- Automatic middleware — logs all HTTP requests with timing
- Error handling — centralized error responses with stack traces
- Panic recovery — graceful handling of runtime panics
- PocketBase integration — uses PocketBase’s native logger
- Selective exclusion — skip logging for static files and service workers
Setup
From core/logging/logging.go:118:
import (
"github.com/magooney-loon/pb-ext/core/logging"
"github.com/magooney-loon/pb-ext/core/server"
)
func main() {
srv := server.New()
// Setup logging with request middleware
logging.SetupLogging(srv)
// Setup panic recovery
srv.App().OnServe().BindFunc(func(e *core.ServeEvent) error {
logging.SetupRecovery(srv.App(), e)
return e.Next()
})
srv.Start()
}
Request Middleware
From core/logging/logging.go:143:
// SetupLogging configures logging using PocketBase's logger
func SetupLogging(srv *server.Server) {
app := srv.App()
requestStats := monitoring.NewRequestStats()
// Create a logger with common application fields
appLogger := app.Logger().With(
"pid", os.Getpid(),
"start_time", time.Now().Format(time.RFC3339),
)
app.OnServe().BindFunc(func(e *core.ServeEvent) error {
logging.SetupErrorHandler(app, e)
e.Router.BindFunc(func(c *core.RequestEvent) error {
defer func() {
RecoverFromPanic(app, c)
}()
// Generate unique trace ID
traceID := security.RandomString(18)
c.Request.Header.Set(TraceIDHeader, traceID)
c.Response.Header().Set(TraceIDHeader, traceID)
start := time.Now()
err := c.Next()
duration := time.Since(start)
logCtx := LogContext{
TraceID: traceID,
StartTime: start,
Method: c.Request.Method,
Path: c.Request.URL.Path,
StatusCode: getStatusCode(c),
Duration: duration,
UserAgent: c.Request.UserAgent(),
IP: c.Request.RemoteAddr,
}
// Track request metrics
if !shouldExcludeFromLogging(logCtx.Path) {
metrics := monitoring.RequestMetrics{
Path: logCtx.Path,
Method: logCtx.Method,
StatusCode: logCtx.StatusCode,
Duration: logCtx.Duration,
Timestamp: logCtx.StartTime,
UserAgent: logCtx.UserAgent,
ContentLength: c.Request.ContentLength,
RemoteAddr: logCtx.IP,
}
requestStats.TrackRequest(metrics)
}
// Log request
if !shouldExcludeFromLogging(logCtx.Path) {
requestLogger := app.Logger().WithGroup("request").With(
"trace_id", logCtx.TraceID,
"method", logCtx.Method,
"path", logCtx.Path,
"status", fmt.Sprintf("%d [%s]", logCtx.StatusCode, monitoring.GetStatusString(logCtx.StatusCode)),
"duration", monitoring.FormatDuration(duration),
"ip", logCtx.IP,
"user_agent", logCtx.UserAgent,
"content_length", c.Request.ContentLength,
"request_rate", requestStats.GetRequestRate(),
)
requestLogger.Debug("Request processed", "event", "http_request")
}
return err
})
return e.Next()
})
}
Trace IDs
From core/logging/logging.go:27:
const (
TraceIDHeader = "X-Trace-ID"
RequestIDKey = "request_id"
)
Every request gets a unique 18-character trace ID:
X-Trace-ID: AbCdEfGhIjKlMnOpQr
Use this ID to correlate logs, errors, and analytics for a single request.
Log Levels
From core/logging/logging.go:19:
type LogLevel int
const (
Debug LogLevel = -4 // Debug level
Info LogLevel = 0 // Info level
Warn LogLevel = 4 // Warning level
Error LogLevel = 8 // Error level
)
Structured Logging
Info Logging
From core/logging/logging.go:75:
func InfoWithContext(ctx context.Context, app core.App, message string, data map[string]interface{}) {
logger := app.Logger()
// Add request ID if available
if ctx != nil {
if id, ok := ctx.Value(RequestIDKey).(string); ok {
logger = logger.With("request_id", id)
}
}
// Create a new logger with all the data fields
for key, value := range data {
logger = logger.With(key, value)
}
logger.Info(message)
}
Usage:
import "github.com/magooney-loon/pb-ext/core/logging"
logging.InfoWithContext(ctx, app, "User action completed", map[string]interface{}{
"user_id": "12345",
"action": "profile_update",
"duration_ms": 42,
})
Error Logging
From core/logging/logging.go:94:
func ErrorWithContext(ctx context.Context, app core.App, message string, err error, data map[string]any) {
logger := app.Logger()
// Add request ID if available
if ctx != nil {
if id, ok := ctx.Value(RequestIDKey).(string); ok {
logger = logger.With("request_id", id)
}
}
// Add error information
if err != nil {
logger = logger.With("error", err.Error())
}
// Add all data fields
for key, value := range data {
logger = logger.With(key, value)
}
logger.Error(message)
}
Usage:
err := processData()
if err != nil {
logging.ErrorWithContext(ctx, app, "Failed to process data", err, map[string]any{
"record_id": recordID,
"operation": "update",
"attempt": 3,
})
}
Direct PocketBase Logger Usage
app.Logger().Info("Simple message")
app.Logger().With(
"user_id", userId,
"action", "login",
).Info("User logged in")
app.Logger().WithGroup("database").With(
"table", "users",
"query_time_ms", 150,
).Debug("Query executed")
Error Handling
From core/logging/error_handler.go:31:
// SetupErrorHandler configures global error handling
func SetupErrorHandler(app core.App, e *core.ServeEvent) {
tmpl, err := template.ParseFS(server.TemplateFS, "templates/error.tmpl")
if err != nil {
app.Logger().Error("Failed to parse error template", "error", err)
}
e.Router.BindFunc(func(c *core.RequestEvent) error {
err := c.Next()
if err == nil {
return nil
}
traceID := c.Request.Header.Get(TraceIDHeader)
statusCode := http.StatusInternalServerError
errorType := "internal_error"
operation := "unknown"
message := err.Error()
// Extract structured error information
var srvErr *server.ServerError
if errors.As(err, &srvErr) {
errorType = srvErr.Type
operation = srvErr.Op
message = srvErr.Message
if srvErr.StatusCode > 0 {
statusCode = srvErr.StatusCode
}
}
// Log error with context
if !shouldExcludeFromLogging(c.Request.URL.Path) {
app.Logger().Error("Request error",
"trace_id", traceID,
"error_type", errorType,
"operation", operation,
"message", message,
"status_code", statusCode,
"path", c.Request.URL.Path,
"method", c.Request.Method,
)
}
// Return JSON or HTML based on request type
response := ErrorResponse{
Status: http.StatusText(statusCode),
Message: message,
Type: errorType,
Operation: operation,
StatusCode: statusCode,
TraceID: traceID,
Timestamp: time.Now().Format(time.RFC3339),
}
// API routes always return JSON
isAPIRoute := strings.HasPrefix(c.Request.URL.Path, "/api/")
if isAPIRoute {
return c.JSON(statusCode, response)
}
// Browser requests get HTML error page
if isBrowserRequest(c.Request) {
var buf bytes.Buffer
if err := tmpl.Execute(&buf, response); err == nil {
return c.HTML(statusCode, buf.String())
}
}
return c.JSON(statusCode, response)
})
}
Error Response Structure
From core/logging/error_handler.go:19:
type ErrorResponse struct {
Status string `json:"status"`
Message string `json:"message"`
Type string `json:"type,omitempty"`
Operation string `json:"operation,omitempty"`
StatusCode int `json:"status_code"`
TraceID string `json:"trace_id"`
Timestamp string `json:"timestamp"`
}
Panic Recovery
From core/logging/error_handler.go:152:
// RecoverFromPanic recovers from panics and returns a 500 response
func RecoverFromPanic(app core.App, c *core.RequestEvent) {
if r := recover(); r != nil {
traceID := c.Request.Header.Get(TraceIDHeader)
if !shouldExcludeFromLogging(c.Request.URL.Path) {
app.Logger().Error("Panic recovered",
"event", "panic",
"trace_id", traceID,
"error", r,
"path", c.Request.URL.Path,
"method", c.Request.Method,
"stack", string(debug.Stack()),
)
}
response := ErrorResponse{
Status: "Internal Server Error",
Message: "A panic occurred while processing your request",
Type: "panic",
Operation: "request_handler",
StatusCode: http.StatusInternalServerError,
TraceID: traceID,
Timestamp: time.Now().Format(time.RFC3339),
}
_ = c.JSON(http.StatusInternalServerError, response)
}
}
Excluded Paths
From core/logging/logging.go:60:
// shouldExcludeFromLogging returns true if the path should be excluded from logging
func shouldExcludeFromLogging(path string) bool {
// Exclude common browser auto-requests
if path == "/service-worker.js" || path == "/favicon.ico" ||
path == "/manifest.json" || path == "/robots.txt" {
return true
}
// Exclude common static file extensions that may 404
for _, ext := range []string{".map", ".ico", ".webmanifest"} {
if len(path) > len(ext) && path[len(path)-len(ext):] == ext {
return true
}
}
return false
}
These paths don’t generate logs to reduce noise from browser auto-requests.
Application Lifecycle Logging
From core/logging/logging.go:128:
appLogger.Info("Application starting up", "event", "app_startup")
app.OnTerminate().BindFunc(func(e *core.TerminateEvent) error {
appLogger.Info("Application shutting down",
"event", "app_shutdown",
"is_restart", e.IsRestart,
"uptime", time.Since(srv.Stats().StartTime).Round(time.Second).String(),
"total_requests", srv.Stats().TotalRequests.Load(),
"avg_request_time_ms", srv.Stats().AverageRequestTime.Load(),
)
return e.Next()
})
Log Context Structure
From core/logging/logging.go:48:
type LogContext struct {
TraceID string
StartTime time.Time
Method string
Path string
StatusCode int
Duration time.Duration
UserAgent string
IP string
}
Example Log Output
Request Log
{
"time": "2026-03-04T15:04:05.123Z",
"level": "DEBUG",
"msg": "Request processed",
"group": "request",
"trace_id": "AbCdEfGhIjKlMnOpQr",
"method": "GET",
"path": "/api/users",
"status": "200 [OK]",
"duration": "15ms",
"ip": "192.168.1.100",
"user_agent": "Mozilla/5.0...",
"content_length": 0,
"request_rate": 5.2,
"event": "http_request"
}
Error Log
{
"time": "2026-03-04T15:04:05.456Z",
"level": "ERROR",
"msg": "Request error",
"trace_id": "XyZaBcDeFgHiJkLmNo",
"error_type": "database_error",
"operation": "find_record",
"message": "record not found",
"status_code": 404,
"path": "/api/users/123",
"method": "GET"
}
Panic Log
{
"time": "2026-03-04T15:04:05.789Z",
"level": "ERROR",
"msg": "Panic recovered",
"event": "panic",
"trace_id": "PqRsTuVwXyZaBcDeFg",
"error": "runtime error: index out of range",
"path": "/api/process",
"method": "POST",
"stack": "goroutine 42 [running]:\n..."
}
Best Practices
- Use trace IDs for request correlation across logs
- Log structured data with key-value pairs instead of concatenated strings
- Include context — user ID, record ID, operation name
- Set appropriate levels — Debug for verbose, Error for failures
- Don’t log sensitive data — passwords, tokens, PII
- Use WithGroup() to organize related logs
- Test panic recovery to ensure graceful error handling
- Monitor log volume — exclude noisy paths if needed
Integration with Error Types
From core/logging/error_handler.go:124:
// HandleContextErrors handles context-related errors
func HandleContextErrors(ctx context.Context, err error, op string) error {
if err == nil {
return nil
}
if ctx != nil && ctx.Err() != nil {
switch ctx.Err() {
case context.DeadlineExceeded:
return monitoring.NewTimeoutError(op, "operation timed out")
case context.Canceled:
return monitoring.NewTimeoutError(op, "operation was canceled")
}
}
var monErr *monitoring.MonitoringError
if errors.As(err, &monErr) {
return err
}
var srvErr *server.ServerError
if errors.As(err, &srvErr) {
return err
}
return monitoring.NewSystemError(op, "unexpected error occurred", err)
}