Development Guide¶
This guide covers development best practices, coding standards, and internal patterns for contributing to NovaEdge.
Table of Contents¶
Logging Standards¶
NovaEdge uses structured logging with zap for high-performance, machine-parseable logs.
Log Levels¶
| Level | Usage | Example |
|---|---|---|
| DEBUG | Detailed diagnostic information, function entry/exit, variable values | Processing request method=GET path=/api |
| INFO | Normal operational messages, state changes, successful operations | Configuration applied version=v1.2.3 |
| WARN | Potentially harmful situations, recoverable errors, fallbacks | Failed to connect to backend, retrying |
| ERROR | Error conditions requiring attention, unrecoverable failures | Failed to apply configuration |
Structured Logging Rules¶
Always use structured fields - never string concatenation:
// Good
logger.Info("Request completed",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.Duration("duration", duration),
)
// Bad - never do this
logger.Info(fmt.Sprintf("Request from %s completed in %v", r.RemoteAddr, duration))
Field Naming Conventions¶
Use consistent snake_case field names:
| Field | Type | Description |
|---|---|---|
correlation_id |
string | Request correlation ID |
cluster |
string | Cluster name |
endpoint |
string | Endpoint address |
method |
string | HTTP method |
path |
string | URL path |
status |
int | HTTP status code |
duration |
duration | Operation duration |
error |
error | Error object (use zap.Error()) |
Correlation IDs¶
Always include correlation IDs for request-scoped logs:
correlationID := uuid.New().String()
ctx := context.WithValue(r.Context(), "correlation_id", correlationID)
logger.Info("Request started",
zap.String("correlation_id", correlationID),
zap.String("method", r.Method),
)
Context Propagation¶
Proper context propagation enables graceful shutdown, timeout handling, and distributed tracing.
Best Practices¶
- Always propagate context - Pass context as the first parameter
- Never use context.Background() in library code - Only in
main()or tests - Derive child contexts - Use
context.WithCancel,context.WithTimeout - Use request context - HTTP handlers should use
r.Context()
Function Signatures¶
// Good - Context as first parameter
func ProcessRequest(ctx context.Context, req *Request) error
// Bad - No context
func ProcessRequest(req *Request) error
HTTP Handlers¶
// Good - Use request context
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Derive child context with timeout
opCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// Pass context to downstream operations
result, err := s.service.Process(opCtx, r)
}
Acceptable Uses of context.Background()¶
- main() function - Top-level context initialization
- Test setup - Creating root context for tests
- Independent background tasks - Tasks with no parent lifecycle
Error Handling¶
NovaEdge uses structured error handling with rich context for debugging.
Custom Error Types¶
Located in internal/pkg/errors/errors.go:
// Network errors
err := pkgerrors.NewNetworkError("connection timeout").
WithField("host", "backend.example.com").
WithField("port", 8080)
// Configuration errors
err := pkgerrors.NewConfigError("invalid gateway spec").
WithField("gateway", "my-gateway")
// Validation errors
err := pkgerrors.NewValidationError("hostname", "required", "hostname cannot be empty")
Sentinel Errors (err113 Compliance)¶
The err113 linter requires all errors to be either wrapped static (sentinel) errors or
standard library errors. Never create dynamic errors with fmt.Errorf("message") — always
define sentinel errors and wrap them:
// Define package-level sentinel errors
var (
errNotInitialized = errors.New("rate limiter not initialized")
errInvalidIP = errors.New("invalid IP address")
errNotIPv4 = errors.New("not an IPv4 address")
)
// Wrap with context using %w
return fmt.Errorf("%w: %s", errInvalidIP, ip)
// Or return directly when no extra context is needed
return errNotInitialized
For errcheck compliance, explicitly ignore error returns in cleanup paths:
For gosec G115 (integer overflow) in low-level code (syscalls, network operations),
use //nolint:gosec with a justification comment when the conversion is provably safe:
Error Wrapping¶
Always wrap errors with context:
Error Checking¶
Use errors.Is() and errors.As():
if errors.Is(err, pkgerrors.ErrConnectionTimeout) {
// Handle timeout
}
var validationErr *pkgerrors.ValidationError
if errors.As(err, &validationErr) {
log.Error("Validation failed", "field", validationErr.Field)
}
Performance Optimizations¶
Connection Pool Configuration¶
Configure connection pools per cluster in internal/agent/upstream/pool.go:
type ConnectionPool struct {
MaxIdleConns int32 // Maximum total idle connections (default: 100)
MaxIdleConnsPerHost int32 // Maximum idle per host (default: 10)
MaxConnsPerHost int32 // Maximum total per host (0 = unlimited)
IdleConnTimeoutMs int64 // Idle timeout in ms (default: 90000)
}
Load Balancer State Caching¶
LB state is cached and only recreated when endpoints change:
// Hash-based change detection
endpointHash := hashEndpointList(endpoints)
if previousHash != endpointHash {
r.loadBalancers[clusterKey] = lb.NewRoundRobin(endpoints)
r.endpointVersions[clusterKey] = endpointHash
}
Impact: ~90% faster config updates when endpoints unchanged.
Metrics Cardinality Reduction¶
Prevent Prometheus metric explosion:
ConfigureMetrics(MetricsConfig{
EnableSampling: true,
SampleRate: 10, // 10% sampling
MaxEndpointCardinality: 100, // Max 100 endpoints per cluster
})
Memory Pools¶
Use sync.Pool for frequently allocated objects:
var responseWriterPool = sync.Pool{
New: func() interface{} {
return &responseWriterWithStatus{statusCode: http.StatusOK}
},
}
// Get from pool
rw := responseWriterPool.Get().(*responseWriterWithStatus)
defer responseWriterPool.Put(rw)
Impact: ~40% reduction in allocations per request.
Benchmarks¶
Run performance benchmarks:
# All benchmarks
make bench
# Specific benchmarks
go test -bench=BenchmarkRouteMatching -benchmem ./internal/agent/router/
Testing Guidelines¶
Test Organization¶
internal/
├── agent/
│ ├── router/
│ │ ├── router.go
│ │ └── router_test.go # Unit tests
│ └── ...
└── controller/
├── controller.go
└── controller_test.go
test/
└── integration/ # Integration tests
└── ...
Running Tests¶
# Run all tests
make test
# Run with coverage
make test-coverage
# Run specific package tests
go test -v ./internal/agent/router/...
# Run integration tests
go test -v ./test/integration/...
Unit Test Patterns¶
func TestRouteMatching(t *testing.T) {
tests := []struct {
name string
path string
expected bool
}{
{"exact match", "/api/v1", true},
{"prefix match", "/api/v1/users", true},
{"no match", "/other", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := router.Match(tt.path)
if result != tt.expected {
t.Errorf("got %v, want %v", result, tt.expected)
}
})
}
}
Testing Context Cancellation¶
func TestContextCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
errCh := make(chan error, 1)
go func() {
errCh <- operation(ctx)
}()
cancel()
err := <-errCh
if err != context.Canceled {
t.Errorf("expected context.Canceled, got %v", err)
}
}
Mocking with Interfaces¶
Use standard Go interfaces for mocking (define them alongside the component under test):
// Production
var forwarder Forwarder = upstream.NewPool(ctx, cluster, endpoints, logger)
// Test
var forwarder Forwarder = &MockForwarder{
ForwardFunc: func(ctx context.Context, req *http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 200}, nil
},
}
TLS Configuration¶
Hardened Defaults¶
All TLS configurations use secure defaults from internal/pkg/tlsutil/tls.go:
- Minimum TLS Version: TLS 1.3 (TLS 1.2 for compatibility)
- Cipher Suites: AEAD ciphers only (AES-GCM, ChaCha20-Poly1305)
- Certificate Validation: Proper CA verification
Creating TLS Configs¶
// Server TLS
config, err := tlsutil.CreateServerTLSConfig(certPEM, keyPEM)
// Client TLS with mTLS
config, err := tlsutil.CreateClientTLSConfigWithMTLS(caCertPEM, clientCertPEM, clientKeyPEM, serverName)
// Backend TLS
config, err := tlsutil.CreateBackendTLSConfig(caCertPEM, serverName, skipVerify)
SNI Support¶
sniConfig := &tlsutil.SNIConfig{
DefaultCert: defaultCert,
Certificates: map[string]*tls.Certificate{
"api.example.com": apiCert,
"*.example.com": wildcardCert,
},
MinVersion: tls.VersionTLS13,
}
config, err := tlsutil.CreateServerTLSConfigWithSNI(sniConfig)
Configuration Validation¶
Use the validator in internal/agent/config/validation.go:
validator := config.NewValidator()
if err := validator.ValidateSnapshot(snapshot); err != nil {
var validationErr *pkgerrors.ValidationError
if errors.As(err, &validationErr) {
log.Error("Validation failed",
"field", validationErr.Field,
"rule", validationErr.Rule,
"message", validationErr.Message,
)
}
return err
}
Code Quality Checklist¶
Before submitting code:
- [ ] All tests pass (
make test) - [ ] No linting errors (
make lint) - [ ] Structured logging with consistent field names
- [ ] Context propagated through all call chains
- [ ] Errors wrapped with context
- [ ] No
context.Background()in library code - [ ] Interface abstractions for testability
- [ ] Benchmarks for performance-critical code
- [ ] Documentation updated if API changed
Related Documentation¶
- Installation Guide - Production deployment
- Architecture Overview - System design