SRouter emphasizes structured, intelligent logging, primarily using the zap library. A *zap.Logger instance is highly recommended in the RouterConfig. If not provided, SRouter will attempt to create a default production logger (or a no-op logger if creation fails). All internal logging performed by the router utilizes this logger instance.
Using a structured logger like zap allows for:
- Machine-Readable Logs: Logs are typically output in formats like JSON, making them easy to parse, index, and query by log aggregation systems (e.g., ELK stack, Splunk, Datadog).
- Contextual Information: Key-value pairs (fields) can be added to log entries, providing rich context (e.g.,
trace_id,user_id,http_method,status_code). - Performance: Zap is designed for high performance and low allocation overhead compared to standard library logging or reflection-based approaches.
SRouter's internal components use appropriate log levels:
Error: Used for significant server-side problems, unrecoverable errors, panics caught by recovery middleware, failures during critical operations (like server shutdown). Status codes 500+ often trigger Error logs.Warn: Used for client-side errors (status codes 400-499), potentially problematic situations (e.g., rate limit exceeded, configuration issues), or notable but non-critical events (e.g., slow request warnings if implemented).Info: Used sparingly for high-level operational information (e.g., server starting, shutting down gracefully). Avoid spamming Info logs in the request path.Debug: Used for detailed diagnostic information useful during development or troubleshooting. This includes detailed request/response metrics (status, duration, bytes, IP, trace ID - logged internally when trace logging is enabled viaTraceIDBufferSize > 0), context values, trace information, and fine-grained steps within components. The main request trace log can also be set to Debug viaTraceLoggingUseInfo: false.
This tiered approach helps manage log verbosity. In production, you typically configure your logger to output Info level and above, while Debug logs are suppressed unless needed for debugging.
You provide the *zap.Logger instance in RouterConfig. You can configure this logger according to your environment's needs.
import "go.uber.org/zap"
import "go.uber.org/zap/zapcore" // Import for zapcore
import "log" // Import standard log package
// Production Logger Example: JSON format, Info level and above
logger, err := zap.NewProduction()
if err != nil {
log.Fatalf("can't initialize zap logger: %v", err)
}
defer logger.Sync() // Flushes buffer, if any
// Development Logger Example: Human-readable console format, Debug level and above
devLogger, err := zap.NewDevelopment()
if err != nil {
log.Fatalf("can't initialize zap logger: %v", err)
}
defer devLogger.Sync()
// Custom Logger Example: Info level, add caller info
config := zap.NewProductionConfig()
config.Level = zap.NewAtomicLevelAt(zap.InfoLevel)
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder // Example time format
customLogger, err := config.Build(zap.AddCaller()) // Add caller info (file:line)
if err != nil {
log.Fatalf("can't initialize zap logger: %v", err)
}
defer customLogger.Sync()
// Pass the chosen logger to SRouter
routerConfig := router.RouterConfig{
Logger: logger, // Use the production logger instance
// ... other config
}Refer to the zap documentation for detailed configuration options.
When trace IDs are enabled via TraceIDBufferSize, SRouter automatically attaches a trace_id field to its logs. Include this ID in your own log entries for full request tracing.
SRouter provides built-in support for trace ID logging, which is crucial for correlating log entries across different parts of your application (and potentially across microservices) for a single incoming request.
When enabled, SRouter automatically assigns a unique trace ID (a UUID) to each incoming request. This trace ID is then:
- Added to the request's context.
- Included in all log messages generated by SRouter's internal logging when trace ID generation is enabled (
TraceIDBufferSize > 0). - Made accessible to your handlers and middleware.
Trace ID generation and injection into the context is enabled by setting TraceIDBufferSize in RouterConfig to a value greater than 0. This internally creates an ID generator and adds the necessary trace middleware.
routerConfig := router.RouterConfig{
Logger: logger, // Assume logger exists
TraceIDBufferSize: 1000, // Enable trace ID generation with a buffer size
// Other configuration...
}
r := router.NewRouter[string, string](routerConfig, authFunction, userIdFromUserFunction) // Assume auth funcs existSetting TraceIDBufferSize to 0 disables automatic trace ID generation and middleware addition.
Alternatively, you can manage the trace middleware explicitly:
- Set
TraceIDBufferSize: 0inRouterConfig. - Create an
*middleware.IDGeneratorinstance usingmiddleware.NewIDGenerator(bufferSize). - Create the middleware using
middleware.CreateTraceMiddleware[UserIDType, UserObjectType](idGen). - Add the created middleware to
RouterConfig.Middlewares(usually as the first one).
import "github.com/Suhaibinator/SRouter/pkg/middleware"
import "github.com/Suhaibinator/SRouter/pkg/common"
idGenerator := middleware.NewIDGenerator(1000) // Create generator
traceMiddleware := middleware.CreateTraceMiddleware[UserIDType, UserObjectType](idGenerator) // Create middleware
routerConfig := router.RouterConfig{
Logger: logger, // Assume logger exists
TraceIDBufferSize: 0, // Disable automatic handling
Middlewares: []common.Middleware{
traceMiddleware, // Add trace middleware explicitly
// Other middleware...
// Note: Internal logging automatically includes the trace ID when TraceIDBufferSize > 0
},
// Other configuration...
}
r := router.NewRouter[string, string](routerConfig, authFunction, userIdFromUserFunction) // Assume auth funcs existOnce enabled (either automatically via TraceIDBufferSize or explicitly via middleware), you can retrieve the trace ID within your handlers or other middleware using helper functions from the pkg/scontext package.
This function retrieves the trace ID from the http.Request object's context. It requires the router's UserIDType and UserObjectType as type parameters.
import (
"fmt"
"net/http"
"github.com/Suhaibinator/SRouter/pkg/scontext" // Use scontext package
// "go.uber.org/zap" // Uncomment if using zap logger example
)
func myHandler(w http.ResponseWriter, r *http.Request) {
// Get the trace ID associated with this request
// Replace string, any with your router's actual UserIDType, UserObjectType
traceID := scontext.GetTraceIDFromRequest[string, any](r)
// Use the trace ID, e.g., in custom logging or downstream requests
fmt.Printf("[trace_id=%s] Handling request for %s\n", traceID, r.URL.Path)
// You can also add it to your own structured logs
// logger.Info("Processing request", zap.String("trace_id", traceID), ...)
// ... rest of handler logic ...
w.Write([]byte("Handled with trace ID: " + traceID))
}If you only have access to the context.Context (e.g., in a function called by your handler), you can use this function. It also requires the router's UserIDType and UserObjectType as type parameters.
import (
"context"
"fmt"
"log"
"net/http"
"github.com/Suhaibinator/SRouter/pkg/scontext" // Use scontext package
)
func processData(ctx context.Context, data string) {
// Get the trace ID from the context passed down from the handler
// Replace string, any with your router's actual UserIDType, UserObjectType
traceID := scontext.GetTraceIDFromContext[string, any](ctx)
// Use the trace ID in logs or further processing
log.Printf("[trace_id=%s] Processing data: %s\n", traceID, data)
// ... processing logic ...
}
// In your handler:
func myHandlerWithContext(w http.ResponseWriter, r *http.Request) {
// Replace string, any with your router's actual UserIDType, UserObjectType
traceID := scontext.GetTraceIDFromRequest[string, any](r) // Get it from request
fmt.Printf("[trace_id=%s] Handler started\n", traceID)
// Pass the request's context (which contains the trace ID) to downstream functions
processData(r.Context(), "some data")
w.Write([]byte("Processed data with trace ID: " + traceID))
}To maintain a consistent trace across multiple services, you should propagate the trace ID when making requests to downstream services. This is typically done by adding the trace ID as an HTTP header (e.g., X-Trace-ID).
import (
"net/http"
"github.com/Suhaibinator/SRouter/pkg/scontext" // Use scontext package
)
func callDownstreamService(r *http.Request) (*http.Response, error) {
// Get the trace ID from the incoming request
// Replace string, any with your router's actual UserIDType, UserObjectType
traceID := scontext.GetTraceIDFromRequest[string, any](r)
// Create a new request to the downstream service
downstreamReq, err := http.NewRequestWithContext(r.Context(), "GET", "http://downstream-service/api/data", nil)
if err != nil {
return nil, err
}
// Add the trace ID to the outgoing request's headers
if traceID != "" {
downstreamReq.Header.Set("X-Trace-ID", traceID) // Common header name
}
// Make the request
client := &http.Client{} // Use a shared client in real applications
resp, err := client.Do(downstreamReq)
// Handle response and error
// ...
return resp, err
}The downstream service should then be configured to look for this header, extract the trace ID, and use it for its own logging and further propagation.
See the examples/trace-logging directory for a runnable example.
When TraceIDBufferSize is set to a value greater than zero, SRouter emits a single log entry at the end of every request. The message is "Request summary statistics" and includes fields:
methodandpath- HTTP
status - request
duration - number of
byteswritten - client
ipanduser_agent trace_idwhen trace IDs are enabled
The log level is selected automatically:
Errorfor status codes>= 500Warnfor status codes>= 400or requests taking longer than 500 msInfofor successful requests whenTraceLoggingUseInfoistrueDebugotherwise
Setting TraceIDBufferSize to 0 disables this automatic summary log.