From c546a3c1a0321f10a776824f40df791bd48f9db3 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sun, 5 Oct 2025 00:20:20 +0300 Subject: [PATCH] refactor: add logging configuration with adjustable log levels and update related documentation --- .env | 1 + .env.example | 8 +++++ Overview.json | 8 +++-- README.md | 62 +++++++++++++++++++++++----------- docs/CONFIGURATION.md | 19 +++++++++++ src/backend/services/config.ts | 12 ++++++- src/backend/services/geoip.ts | 2 +- src/backend/services/logger.ts | 33 +++++++++++++++--- src/backend/types/index.ts | 5 +++ src/backend/types/log.ts | 15 ++++++++ 10 files changed, 137 insertions(+), 28 deletions(-) create mode 100644 src/backend/types/log.ts diff --git a/.env b/.env index 4820fb6..c4e04f0 100644 --- a/.env +++ b/.env @@ -13,6 +13,7 @@ PORT=5172 BASE_PATH=/ NODE_ENV=development ENABLE_HTTPS_SECURITY=false +LOG_LEVEL=debug # Database Path - Local MaxMind databases location MAXMIND_DB_PATH=D:\\tools\\maxmind-dbs diff --git a/.env.example b/.env.example index 93b49f4..079cceb 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,14 @@ PORT=5172 BASE_PATH=/ NODE_ENV=production +# Logging Configuration +# Minimum log level: debug, info, warning, error +# - debug: All logs (very verbose) +# - info: Info, warnings, and errors (default for development) +# - warning: Only warnings and errors (recommended for production) +# - error: Only errors +LOG_LEVEL=warning + # Database Path (usually doesn't need to change) MAXMIND_DB_PATH=/usr/share/maxmind diff --git a/Overview.json b/Overview.json index 085950e..67b355d 100644 --- a/Overview.json +++ b/Overview.json @@ -208,8 +208,8 @@ "**HTTPS Security Headers** - Toggle security headers (CSP, HSTS, COOP, CORP) based on deployment type (HTTP vs HTTPS)", "**Port Configuration** - Customize backend and frontend ports", "**Base Path Support** - Deploy under custom URL paths for reverse proxy scenarios", - "**Logging Levels** - Adjust verbosity from debug to production", - "**Seq Integration** - Optional structured logging to Seq server", + "**Configurable Logging Levels** - Hierarchical log filtering (debug, info, warning, error) similar to Serilog in .NET", + "**Seq Integration** - Optional structured logging to Seq server with configurable minimum level", "**Database Path** - Configurable GeoLite2 database location" ] }, @@ -217,12 +217,14 @@ "title": "Monitoring & Observability", "items": [ "**Structured Logging** - JSON-formatted logs with context and correlation IDs", + "**Hierarchical Log Levels** - Configurable minimum log level (debug, info, warning, error) for filtering", "**Version Endpoint** - Check deployed version, build date, and git revision", "**Health Checks** - Monitor service availability and database status", "**Error Tracking** - Detailed error messages with stack traces in development", "**Request Logging** - Log all API requests with IP, method, path, and response time", "**Rate Limit Metrics** - Track rate limit hits and blocked requests", - "**Seq Integration** - Optional centralized logging with query and dashboard capabilities" + "**Seq Integration** - Optional centralized logging with query and dashboard capabilities", + "**Production-Ready Logging** - Minimal overhead with filtered log levels for production environments" ] }, { diff --git a/README.md b/README.md index f8cbae4..ee8caea 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ PORT=5172 BASE_PATH=/ NODE_ENV=development ENABLE_HTTPS_SECURITY=false +LOG_LEVEL=debug # debug | info | warning | error # Database Path - Point to your MaxMind databases location MAXMIND_DB_PATH=D:\tools\maxmind-dbs # Windows @@ -98,13 +99,14 @@ This will start: #### 1. Configure Environment -Edit `.env` file with your MaxMind credentials: +Edit `.env` file with your MaxMind credentials and logging level: ```env GEOIPUPDATE_ACCOUNT_ID=your_account_id_here GEOIPUPDATE_LICENSE_KEY=your_license_key_here FRONTEND_API_KEY=your-secure-frontend-api-key-here EXTERNAL_API_KEYS=your-secure-external-api-key-1,your-secure-external-api-key-2 +LOG_LEVEL=warning # Recommended for production (debug | info | warning | error) ``` #### 2. Start Services @@ -113,6 +115,28 @@ EXTERNAL_API_KEYS=your-secure-external-api-key-1,your-secure-external-api-key-2 docker-compose up -d ``` +**Or with Docker run:** + +```bash +# Build image +docker build -t bitip-geoip:latest . + +# Run with custom log level +docker run -d \ + -p 5172:5172 \ + -e LOG_LEVEL=warning \ + -e FRONTEND_API_KEY=your-api-key \ + -v /path/to/maxmind-dbs:/usr/share/maxmind \ + bitip-geoip:latest +``` + +**Available log levels:** + +- `debug` - All logs (verbose, for development/troubleshooting) +- `info` - Info + warnings + errors (default) +- `warning` - Warnings + errors only (recommended for production) +- `error` - Errors only (minimal logging) + #### 3. Access the Service - **Web Interface**: http://localhost:5172 @@ -323,24 +347,24 @@ npm run install:all # Install all dependencies (root + backend + frontend) ### Environment Variables -| Variable | Default | Description | -| -------------------------- | ---------------------- | --------------------------------------------------------------- | -| `PORT` | `5172` | Server port | -| `BASE_PATH` | `/` | Application base path | -| `NODE_ENV` | `development` | Environment mode (development/production) | -| `ENABLE_HTTPS_SECURITY` | `false` | Enable HTTPS security headers (CSP, HSTS, COOP, CORP) | -| `MAXMIND_DB_PATH` | `/usr/share/maxmind` | MaxMind database directory | -| `FRONTEND_API_KEY` | `frontend-default-key` | Frontend API key | -| `EXTERNAL_API_KEYS` | `external-default-key` | External API keys (comma-separated) | -| `FRONTEND_ALLOWED_ORIGINS` | `http://localhost:*` | Allowed CORS origins for frontend (comma-separated) | -| `FRONTEND_RATE_WINDOW_MS` | `60000` | Frontend rate limit window (milliseconds) | -| `FRONTEND_RATE_MAX` | `30` | Frontend rate limit max requests | -| `EXTERNAL_RATE_WINDOW_MS` | `60000` | External rate limit window (milliseconds) | -| `EXTERNAL_RATE_MAX` | `1000` | External rate limit max requests | -| `BATCH_LIMIT` | `100` | Maximum IPs per batch request | -| `DEBOUNCE_MS` | `2000` | Frontend input debounce delay (milliseconds) | -| `SEQ_URL` | - | Seq logging server URL (optional) | -| `SEQ_API_KEY` | - | Seq API key (optional) | +| Variable | Default | Description | +| -------------------------- | ---------------------- | ----------------------------------------------------- | +| `PORT` | `5172` | Server port | +| `BASE_PATH` | `/` | Application base path | +| `NODE_ENV` | `development` | Environment mode (development/production) | +| `ENABLE_HTTPS_SECURITY` | `false` | Enable HTTPS security headers (CSP, HSTS, COOP, CORP) | +| `MAXMIND_DB_PATH` | `/usr/share/maxmind` | MaxMind database directory | +| `FRONTEND_API_KEY` | `frontend-default-key` | Frontend API key | +| `EXTERNAL_API_KEYS` | `external-default-key` | External API keys (comma-separated) | +| `FRONTEND_ALLOWED_ORIGINS` | `http://localhost:*` | Allowed CORS origins for frontend (comma-separated) | +| `FRONTEND_RATE_WINDOW_MS` | `60000` | Frontend rate limit window (milliseconds) | +| `FRONTEND_RATE_MAX` | `30` | Frontend rate limit max requests | +| `EXTERNAL_RATE_WINDOW_MS` | `60000` | External rate limit window (milliseconds) | +| `EXTERNAL_RATE_MAX` | `1000` | External rate limit max requests | +| `BATCH_LIMIT` | `100` | Maximum IPs per batch request | +| `DEBOUNCE_MS` | `2000` | Frontend input debounce delay (milliseconds) | +| `SEQ_URL` | - | Seq logging server URL (optional) | +| `SEQ_API_KEY` | - | Seq API key (optional) | ### MaxMind Database Setup diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 8034313..57999b1 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -120,6 +120,25 @@ ENABLE_HTTPS_SECURITY=false - `production` - Optimized for performance, minimal logging - **Example**: `production` +**`LOG_LEVEL`** + +- **Purpose**: Minimum logging level for both console and Seq output +- **Format**: String +- **Required**: No +- **Default**: `info` +- **Valid Values**: + - `debug` - All logs including debug messages (very verbose, use only for troubleshooting) + - `info` - Informational messages, warnings, and errors (recommended for development) + - `warning` - Only warnings and errors (recommended for production) + - `error` - Only error messages (minimal logging) +- **Use Cases**: + - Development: `debug` or `info` - Full visibility into application behavior + - Production: `warning` or `error` - Reduce log volume and focus on issues + - Troubleshooting: Temporarily set to `debug` to diagnose problems +- **Example**: `warning` +- **Note**: Similar to Serilog's minimum level filtering in .NET +- **Impact**: Affects both console output and Seq structured logging + **`ENABLE_HTTPS_SECURITY`** - **Purpose**: Enable/disable HTTPS security headers (Helmet middleware) diff --git a/src/backend/services/config.ts b/src/backend/services/config.ts index 50d4fdb..ac383f6 100644 --- a/src/backend/services/config.ts +++ b/src/backend/services/config.ts @@ -1,4 +1,4 @@ -import { Config } from '../types/index.js'; +import { Config, LogLevel, LOG_LEVELS } from '../types/index.js'; import path from 'path'; import { config as dotenvConfig } from 'dotenv'; import { fileURLToPath } from 'url'; @@ -10,12 +10,22 @@ const __dirname = dirname(__filename); // Load .env file from root directory dotenvConfig({ path: path.join(__dirname, '../../../.env') }); +// Parse log level from environment variable +const parseLogLevel = (level?: string): LogLevel => { + const normalized = (level || LOG_LEVELS.INFO).toLowerCase(); + const validLevels = Object.values(LOG_LEVELS); + return validLevels.includes(normalized as LogLevel) + ? (normalized as LogLevel) + : LOG_LEVELS.INFO; +}; + export const config: Config = { port: parseInt(process.env.PORT || '5172', 10), basePath: process.env.BASE_PATH || '/', maxmindDbPath: process.env.MAXMIND_DB_PATH || '/usr/share/maxmind', seqUrl: process.env.SEQ_URL, seqApiKey: process.env.SEQ_API_KEY, + logLevel: parseLogLevel(process.env.LOG_LEVEL), enableHttpsSecurity: process.env.ENABLE_HTTPS_SECURITY === 'true', apiKeys: { frontend: process.env.FRONTEND_API_KEY || 'frontend-default-key', diff --git a/src/backend/services/geoip.ts b/src/backend/services/geoip.ts index f01e7bb..e54aa80 100644 --- a/src/backend/services/geoip.ts +++ b/src/backend/services/geoip.ts @@ -96,7 +96,7 @@ class GeoIPService { const response: City = this.cityReader!.city(ip); const result: DetailedGeoIPResponse = { ip, - location: response as any as GeoIPLocation, + location: response as GeoIPLocation, }; this.cache.set(cacheKey, result); diff --git a/src/backend/services/logger.ts b/src/backend/services/logger.ts index 0cdf53a..42e4592 100644 --- a/src/backend/services/logger.ts +++ b/src/backend/services/logger.ts @@ -1,10 +1,14 @@ import { Logger as SeqLogger, SeqLoggerConfig } from 'seq-logging'; +import { LogLevel, LOG_LEVELS, LOG_LEVEL_PRIORITY } from '../types/index.js'; import config from './config.js'; class Logger { private seqLogger?: SeqLogger; + private minLevel: LogLevel; constructor() { + this.minLevel = config.logLevel; + if (config.seqUrl) { const seqConfig: SeqLoggerConfig = { serverUrl: config.seqUrl, @@ -15,9 +19,20 @@ class Logger { }; this.seqLogger = new SeqLogger(seqConfig); } + + // Log initial configuration + console.log( + `[INFO] Logger initialized with minimum level: ${this.minLevel.toUpperCase()}` + ); } - info(message: string, properties?: any): void { + private shouldLog(level: LogLevel): boolean { + return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[this.minLevel]; + } + + info(message: string, properties?: Record): void { + if (!this.shouldLog(LOG_LEVELS.INFO)) return; + console.log(`[INFO] ${message}`, properties || ''); if (this.seqLogger) { this.seqLogger.emit({ @@ -29,7 +44,9 @@ class Logger { } } - warn(message: string, properties?: any): void { + warn(message: string, properties?: Record): void { + if (!this.shouldLog(LOG_LEVELS.WARNING)) return; + console.warn(`[WARN] ${message}`, properties || ''); if (this.seqLogger) { this.seqLogger.emit({ @@ -41,7 +58,13 @@ class Logger { } } - error(message: string, error?: Error, properties?: any): void { + error( + message: string, + error?: Error, + properties?: Record + ): void { + if (!this.shouldLog(LOG_LEVELS.ERROR)) return; + console.error(`[ERROR] ${message}`, error || '', properties || ''); if (this.seqLogger) { this.seqLogger.emit({ @@ -57,7 +80,9 @@ class Logger { } } - debug(message: string, properties?: any): void { + debug(message: string, properties?: Record): void { + if (!this.shouldLog(LOG_LEVELS.DEBUG)) return; + console.debug(`[DEBUG] ${message}`, properties || ''); if (this.seqLogger) { this.seqLogger.emit({ diff --git a/src/backend/types/index.ts b/src/backend/types/index.ts index 27a6610..d88d384 100644 --- a/src/backend/types/index.ts +++ b/src/backend/types/index.ts @@ -1,3 +1,7 @@ +import { LogLevel } from './log'; + +export * from './log'; + export interface GeoIPLocation { country?: { iso_code?: string; @@ -69,6 +73,7 @@ export interface Config { maxmindDbPath: string; seqUrl?: string; seqApiKey?: string; + logLevel: LogLevel; enableHttpsSecurity: boolean; apiKeys: { frontend: string; diff --git a/src/backend/types/log.ts b/src/backend/types/log.ts new file mode 100644 index 0000000..ae02179 --- /dev/null +++ b/src/backend/types/log.ts @@ -0,0 +1,15 @@ +export const LOG_LEVELS = { + DEBUG: 'debug', + INFO: 'info', + WARNING: 'warning', + ERROR: 'error', +} as const; + +export type LogLevel = (typeof LOG_LEVELS)[keyof typeof LOG_LEVELS]; + +export const LOG_LEVEL_PRIORITY: Record = { + [LOG_LEVELS.DEBUG]: 0, + [LOG_LEVELS.INFO]: 1, + [LOG_LEVELS.WARNING]: 2, + [LOG_LEVELS.ERROR]: 3, +} as const;