RaidHub Services uses a centralized logging system with structured log levels and consistent naming conventions. Each service has its own logger to ensure proper service identification and organized output.
Each package should declare a logger at the package level using descriptive names. Do not create separate logger.go files - declare loggers directly in your main package files.
package myservice
import (
"raidhub/lib/utils/logging"
// other imports...
)
var logger = logging.NewLogger("SERVICE_NAME")
// Your service code here...See lib/services/pgcr_processing/process-pgcr.go:
package pgcr_processing
import (
"errors"
"fmt"
"raidhub/lib/dto"
"raidhub/lib/utils/logging"
// other imports...
)
var logger = logging.NewLogger("PGCR_PROCESSING_SERVICE")
func ProcessPGCR(pgcr *bungie.DestinyPostGameCarnageReport) (*dto.ProcessedInstance, PGCRResult) {
logger.Debug("STARTING_PGCR_PROCESSING", map[string]any{
"instanceId": pgcr.ActivityDetails.InstanceId,
})
// ... processing logic
}-
Services (
lib/services/): Use*_SERVICEsuffixINSTANCE_STORAGE_SERVICECHEAT_DETECTION_SERVICEPGCR_PROCESSING_SERVICEPLAYER_SERVICECHARACTER_SERVICEINSTANCE_SERVICECLAN_SERVICE
-
Infrastructure (
lib/database/,lib/messaging/, etc.): Use component namePOSTGRESMONITORINGMIGRATIONS(in lib/migrations) orMigrations(in lib/database/migrations)
-
Applications (
apps/): Use Greek mythology names (matching app names)atlas(main logger)atlas::metricsService(sub-logger)atlas::offloadWorker(sub-logger)hermeszeus
-
Tools (
tools/): Use tool-specific names (SCREAMING_SNAKE_CASE)MISSED_PGCRMANIFEST_DOWNLOADERLEADERBOARD_CLAN_CRAWLCHEAT_DETECTIONREFRESH_VIEW_TOOLTOOLS(for general tool logging)FLAG_RESTRICTED_TOOLPROCESS_PGCR_TOOLFIX_SHERPA_TOOLUPDATE_SKULL_TOOLSEEDMIGRATIONSorMigrations(depending on package)
-
Web Clients (
lib/web/): Use service name with_CLIENTBUNGIE_CLIENTPROMETHEUS_API_CLIENT
The logging system supports configurable log levels to control verbosity. Log levels can be set via:
-
Environment Variable:
LOG_LEVEL(recommended for production)export LOG_LEVEL=warn # Only show warnings and errors
-
CLI Flags:
-log-levelor-log(for tools and services)./bin/hermes --log-level=debug ./bin/atlas --log debug
-
Verbose Flag:
-vor-verbose(equivalent todebuglevel)./bin/hermes --verbose
Available Log Levels (in order of severity):
debug- Most verbose, includes all DEBUG logsinfo- Default level, shows operational informationwarn- Only warnings and errorserror- Only errors (includes FATAL logs)
When a log level is set, only logs at that level or higher will be output. For example, setting LOG_LEVEL=warn will show WARN and ERROR logs (including FATAL), but hide INFO and DEBUG logs.
Note: Fatal is not a separate configurable log level - fatal logs are always shown when error level is enabled. The Fatal() method will always exit the application after logging.
Logs can be redirected to files while still maintaining console output. This is useful for:
- Log aggregation and analysis
- Long-term log storage
- Separating logs from application output
Environment Variables:
STDOUT- Redirects INFO and DEBUG logs to a file (in addition to console)STDERR- Redirects WARN, ERROR, and FATAL logs to a file (in addition to console)
Behavior:
When STDOUT or STDERR environment variables are set, logs are written to both the file and the original console output. This ensures logs are always visible in the console while also being persisted to files.
# Write logs to files while keeping console output
export STDOUT=./logs/app.log
export STDERR=./logs/errors.log
./bin/hermes
# Logs appear both in console and in filesFile Handling:
- Files are created automatically if they don't exist
- Logs are appended to existing files (no truncation)
- If file creation fails, the application will panic on startup
All log entries include an RFC3339Nano timestamp at the beginning of each log line:
2024-01-15T10:30:45.123456789Z [INFO][SERVICE_NAME] -- MESSAGE key=value key2=value2
The timestamp format is time.RFC3339Nano, providing nanosecond precision for accurate timing analysis and correlation across services.
The logging system handles errors gracefully:
- Non-nil errors: Automatically added to fields with key
"error"usingerr.Error() - Nil errors: Added to fields as
"error": "<nil>"to maintain consistent log structure - This ensures all error fields are present in logs, making queries and filtering more reliable
- Purpose: Detailed information for debugging and troubleshooting
- Usage: Only logged when log level is set to
debug(viaLOG_LEVEL=debug,--verbose,-v,--log-level=debug, or--log debug) - Persistence: Only shown when debugging - hidden by default
- Examples:
- Variable values during processing
- Detailed API request/response data
- Step-by-step algorithm execution
- Internal state information
logger.Debug("REQUEST_PROCESSING", map[string]any{
"userId": userId,
"stage": "validation",
"requestData": data,
})
logger.Debug("CACHE_OPERATION", map[string]any{
"type": "hit",
"key": key,
"value": value,
})Note: DEBUG logs should be implemented to respect verbose flags in applications.
- Purpose: Important operational information for monitoring and tracking
- Usage: Important events, successful operations, system state changes
- Persistence: PERSISTED - expect these logs to be searchable
- Examples:
- Service startup/shutdown
- Successful database connections
- Important business logic milestones
- Performance metrics
logger.Info("SERVICE_STARTED", map[string]any{
"port": 8080,
"status": "ready",
})
logger.Info("BATCH_PROCESSED", map[string]any{
"type": "pgcr",
"count": count,
"duration": duration,
})- Purpose: Issues that should be monitored but don't require alerts
- Usage: Problems that don't crash the app but need attention
- Persistence: PERSISTED - logged for monitoring and analysis
- Examples:
- API failures that are retried
- Data inconsistencies
- Performance degradation
- External service errors
- Business logic violations
logger.Warn("API_CONNECTION_FAILED", err, map[string]any{
"service": "bungie",
"attempt": attemptCount,
"action": "retrying",
})
logger.Warn("INVALID_DATA_DETECTED", err, map[string]any{
"entity": "player",
"playerId": playerId,
"issue": "completion_data",
})
// Can pass nil if there's no error
logger.Warn("PERFORMANCE_DEGRADATION", nil, map[string]any{
"response_time": "2s",
"threshold": "500ms",
})- Purpose: Errors that should be monitored and alerted on (Sentriable errors)
- Usage: Problems that need immediate attention but don't crash the app
- Persistence: PERSISTED and ALERTED - expect these to trigger Sentry alerts
- Examples:
- Critical business logic failures
- Data corruption issues
- Authentication/authorization failures
- External service dependencies failing
- Operations that must succeed but failed
logger.Error("AUTHENTICATION_FAILED", err, map[string]any{
"user_id": userId,
"action": "access_denied",
})
logger.Error("DATA_CORRUPTION_DETECTED", err, map[string]any{
"entity": "instance",
"instance_id": instanceId,
"issue": "invalid_completion_data",
})
// Error is automatically added to fields with key "error" if provided- Purpose: Unrecoverable errors that require the application to crash
- Usage: Critical system failures where the app cannot continue safely
- Log Level: Treated as
errorlevel for filtering purposes - Persistence: PERSISTED and ALERTED - logs then CRASHES with
os.Exit(1) - Examples:
- Database connection failures during startup
- Critical configuration missing
- System resource exhaustion
- Programming errors that violate invariants
logger.Fatal("DATABASE_CONNECTION_FAILED", err, map[string]any{
"phase": "startup",
"type": "postgresql",
})
logger.Fatal("CONFIGURATION_MISSING", nil, map[string]any{
"key": configKey,
"phase": "startup",
"severity": "critical",
})
// Error is automatically added to fields with key "error" if providedNote: Fatal is not a separate configurable log level. Fatal logs are always shown when the log level is set to error or lower. The Fatal() method will always exit the application after logging, regardless of log level configuration.
Use logger.Warn() for problems that:
- Don't crash the application
- Should be monitored but don't require immediate alerts
- Can be handled gracefully (retries, fallbacks, etc.)
- Indicate potential issues that need tracking
Use logger.Error() for problems that:
- Don't crash the application
- Require immediate attention via Sentry alerts
- Indicate serious operational problems
- Need prompt investigation and resolution
// Example: API failure with retry (monitoring)
if err := externalAPI.Call(); err != nil {
logger.Warn("EXTERNAL_API_CALL_FAILED", err, map[string]any{
"action": "retrying",
})
// Continue with retry logic
}
// Example: Critical authentication failure (requires alert)
if err := validateUserPermissions(userId); err != nil {
logger.Error("USER_PERMISSION_VALIDATION_FAILED", err, map[string]any{
"userId": userId,
"action": "access_denied",
})
return fmt.Errorf("access denied: %w", err)
}Use logger.Fatal() for problems that:
- Make the application unable to continue safely
- Require immediate restart/intervention
- Indicate critical system failures
// Example: Critical startup failure
if err := database.Connect(); err != nil {
logger.Fatal("DATABASE_CONNECTION_FAILED", err, map[string]any{
"phase": "startup",
})
// Application crashes here with os.Exit(1)
}- User input errors - return error instead
- Individual request failures - use ERROR (if critical) or WARN and continue
- Data processing errors - use ERROR (if serious) or WARN and skip item
- Expected business logic failures - use ERROR (if needs alerts) or WARN/INFO
| Method | Signature | Usage | Output | Respects Log Level |
|---|---|---|---|---|
Info() |
Info(key string, fields map[string]any) |
Operational information | stdout | Yes |
Warn() |
Warn(key string, err error, fields map[string]any) |
Issues needing attention | stderr | Yes |
Error() |
Error(key string, err error, fields map[string]any) |
Sentry alerts | stderr | Yes |
Debug() |
Debug(key string, fields map[string]any) |
Verbose flag only | stdout | Yes |
Fatal() |
Fatal(key string, err error, fields map[string]any) |
Logs then crashes | stderr | Yes (treated as error level) |
Output Behavior:
- INFO and DEBUG logs are written to stdout (or both stdout and file if
STDOUTis set) - WARN, ERROR, and FATAL logs are written to stderr (or both stderr and file if
STDERRis set) - All logging methods respect the configured log level - logs below the current level are not output
Fatallogs are shown when log level iserroror lower (fatal is not a separate configurable level)
Error Parameter:
Warn(),Error(), andFatal()methods accept anerroras the second parameter- If the error is not
nil, it is automatically added to the fields map with the key"error"(usingerr.Error()) - If the error is
nil, it is added to fields as"error": "<nil>"to maintain consistent log structure - You can pass
nilif there's no error to log (useful for warnings that don't have an associated error) - The
fieldsparameter can benilif you only want to log the error
Parameters:
- Message: SCREAMING_UPPER_CASE string for the event
- Error: Optional error to include in the log (automatically added to fields with key "error")
- Fields: Structured key-value pairs using
map[string]any{}(can benil)
ALWAYS use key-value pairs - never plain strings:
// ✅ GOOD - Structured with context
logger.Info("DATABASE_CONNECTED", map[string]any{
"type": "postgresql",
})
logger.Info("USER_LOGIN", map[string]any{
"userId": 12345,
"ip": "192.168.1.1",
"status": "success",
})
logger.Info("REQUEST_PROCESSED", map[string]any{
"method": "POST",
"endpoint": "/api/users",
"duration": "150ms",
})
// ❌ BAD - Plain strings (hard to search/filter)
logger.Info("DATABASE_CONNECTION_ESTABLISHED", nil)
logger.Info("USER_LOGIN_SUCCESSFUL", nil)
logger.Info("USER_LOGIN", nil) // Still not structured - missing fields!Why structured logging matters:
- Searchable:
grep 'type.*postgresql'finds all postgres connections - Filterable: Log aggregators can filter by key-value pairs
- Contextual: Always includes relevant metadata for debugging
Always include relevant context with errors. The error parameter is automatically added to fields:
logger.Warn("DATABASE_QUERY_FAILED", err, map[string]any{
"query": "SELECT * FROM users",
"params": params,
})
// Error is automatically added to fields with key "error"- DEBUG logs should not impact production performance (only shown with verbose flag)
- Use structured logging over string formatting when possible
- Avoid logging large objects at INFO level
- Never log sensitive data: passwords, API keys, tokens, PII
- Redact or hash sensitive fields when logging is necessary
- Be careful with user-generated content
// ✅ GOOD - Structured with safe data
logger.Info("API_REQUEST_COMPLETED", map[string]any{
"method": "GET",
"endpoint": "/api/users",
"userId": userId,
"status": 200,
})
// ❌ BAD - Contains sensitive data
logger.Info("API_REQUEST", map[string]any{
"headers": headers,
"body": body,
})
// ❌ BAD - Plain string (not searchable)
logger.Info("API_REQUEST_COMPLETED_SUCCESSFULLY", nil)// Before
import "log"
log.Println("message") → logger.Info("MESSAGE", nil)
log.Printf("msg %s", var) → logger.Info("MESSAGE", map[string]any{"var": var})
log.Fatalf("err: %v", err) → logger.Fatal("ERROR", err, nil)
// Setup
import "raidhub/lib/utils/logging"
var logger = logging.NewLogger("SERVICE_NAME")Benefits: Service identification, Sentry integration, proper log levels, structured logging
- DEBUG: Only shown when log level is
debug(viaLOG_LEVELor--verboseflag) - INFO: Shown at default
infolevel or higher, persisted for operational visibility - WARN: Shown at
warnlevel or higher, persisted for monitoring and analysis - ERROR: Shown at
errorlevel, persisted and triggers Sentry alerts - FATAL: Shown at
errorlevel (not a separate configurable level), persisted, triggers alerts, then crashes app
All logs are structured using logfmt format for easy querying and analysis. Logs respect the configured log level and can be redirected to files via STDOUT and STDERR environment variables.
Fields prefixed with $ in log entries are automatically converted to Sentry tags for better filtering and alerting. This allows you to add high-cardinality fields (like queue names) as tags without cluttering the "extra" data.
Usage:
logger.Error("PROCESSING_ERROR", err, map[string]any{
"$queue": "player_crawl", // Becomes Sentry tag "queue"
"membership_id": membershipId, // Remains in "extra" data
"retry_count": 3, // Remains in "extra" data
})Behavior:
- Fields with
$prefix are extracted and set as Sentry tags - The
$prefix is removed from the tag name (e.g.,$queuebecomes tagqueue) - Tag fields are removed from the "extra" data to avoid duplication
- Tags are useful for filtering and grouping errors in Sentry
- Use tags for low-cardinality fields that you want to filter by (queue names, worker IDs, etc.)
- Use regular fields for high-cardinality or detailed data that should remain in "extra"
Example:
// In worker.go
fields := map[string]any{
"$queue": w.QueueName, // Tag in Sentry
"membership_id": membershipId, // Extra data in Sentry
"retry_count": retryCount, // Extra data in Sentry
}
logger.Error("MESSAGE_PROCESSING_ERROR", err, fields)Note: The $queue constant is defined in lib/utils/sentry for consistency, but any field starting with $ will be treated as a tag.
ContextCancelledError (from retry operations) is automatically excluded from Sentry reporting since it's an expected condition when workers are shutting down or being scaled in. These errors are still logged but won't trigger Sentry alerts.
By default, Promtail is configured to scrape logs from Docker containers via Docker service discovery.
Start via Tilt (recommended):
tilt upOr start via Docker Compose only:
docker-compose up -dWhen running services as native processes (not in Docker), Promtail won't collect logs by default because it's configured for Docker service discovery.
To enable log collection in production:
-
Configure services to write logs to files using
STDOUTandSTDERRenvironment variables:export STDOUT=/var/log/raidhub/app.log export STDERR=/var/log/raidhub/errors.log
-
Update Promtail configuration (
infrastructure/promtail/promtail.yml) to read from filesystem instead of Docker:- Comment out the
docker_sd_configssection - Uncomment and configure the
file_logsjob to point to your log file paths - Ensure Promtail has read access to the log files
- Comment out the
-
Deploy Loki and Promtail separately (as systemd services, Kubernetes DaemonSet, or standalone processes)
- Grafana: http://localhost:${GRAFANA_PORT}
- Loki: http://localhost:${LOKI_PORT}
- Errors by source (last 5m):
sum by (source) (count_over_time({level="ERROR"}[5m]))
- Errors by logger (last 5m):
sum by (logger) (count_over_time({level="ERROR"}[5m]))
- Recent logs from hermes container:
{source="hermes"} | line_format "{{.line}}" | limit 100
- Logs from HERMES logger (regardless of source):
{logger="HERMES"} | line_format "{{.line}}" | limit 100
- Warnings or errors from atlas container:
{source="atlas", level=~"WARN|ERROR"}
source: Docker Compose service name (hermes, atlas, zeus, postgres, rabbitmq, clickhouse, etc.)logger: Logger name from log prefix[LOGGER](HERMES, ATLAS, ZEUS, POSTGRES, etc.)level: Log level (INFO, WARN, ERROR, FATAL, DEBUG)container: Docker container name
- Log format is preserved (structured text) and parsed by Promtail.
- Tilt now runs
hermes,atlas,zeusas Docker services, so Promtail collects logs via Docker service discovery. - Use
sourceto filter by which container/app emitted the log. - Use
loggerto filter by the logger name used in the code.
// lib/services/cheat_detection/
import (
"raidhub/lib/utils/logging"
)
// Constants or strings work, but they must be SCREAMING_SNAKE_CASE
const (
SUSPICIOUS_ACTIVITY_DETECTED = "SUSPICIOUS_ACTIVITY_DETECTED"
DATABASE_CONNECTION_FAILED = "DATABASE_CONNECTION_FAILED"
)
var logger = logging.NewLogger("CHEAT_DETECTION_SERVICE")
logger.Debug("STARTING_CHEAT_DETECTION_ANALYSIS", map[string]any{
logging.MEMBERSHIP_ID: membershipId,
logging.TYPE: "behavioral_analysis",
})
logger.Warn(SUSPICIOUS_ACTIVITY_DETECTED, nil, map[string]any{
logging.MEMBERSHIP_ID: playerId,
logging.TYPE: "stat_anomaly",
logging.ACTION: "flagged_for_review",
})
logger.Fatal(DATABASE_CONNECTION_FAILED, err, map[string]any{
logging.TYPE: "postgresql",
logging.OPERATION: "query_player_stats",
})// apps/hermes/main.go
import (
"raidhub/lib/utils/logging"
)
// Constants for major lifecycle events
const (
STARTING_TOPIC = "STARTING_TOPIC"
STARTED_TOPIC = "STARTED_TOPIC"
)
var HermesLogger = logging.NewLogger("hermes")
HermesLogger.Info(STARTED_TOPIC, map[string]any{
"topic": "instance_store",
"mode": "all",
})
HermesLogger.Info("MISSED_PGCRS_FOUND", map[string]any{
logging.COUNT: count,
logging.TYPE: "pgcr",
logging.ACTION: "queued_for_processing",
})// lib/database/postgres/
import (
"raidhub/lib/utils/logging"
)
var logger = logging.NewLogger("POSTGRES")
logger.Info("POSTGRES_CONNECTED", map[string]any{
logging.STATUS: "ready",
})
logger.Warn("POSTGRES_CONNECTION_POOL_APPROACHING_LIMIT", nil, map[string]any{
logging.COUNT: count,
logging.TYPE: "active_connections",
logging.ACTION: "monitor_pool_usage",
})This logging system provides consistent, searchable, and monitorable logs across all RaidHub Services while maintaining clear service boundaries and appropriate log levels for different operational needs.