diff --git a/docs/docs/features/monitoring.md b/docs/docs/features/monitoring.md index f087a3306f..46063fded6 100644 --- a/docs/docs/features/monitoring.md +++ b/docs/docs/features/monitoring.md @@ -112,4 +112,40 @@ You can then make a new panel, specifying Prometheus as the data source for it. -- TODO: add images and more details here +## Structured Logging + +In addition to Prometheus metrics, Immich supports structured JSON logging which is ideal for log aggregation systems like Grafana Loki, ELK Stack, Datadog, Splunk, and others. + +### Configuration + +By default, Immich outputs human-readable console logs. To enable JSON logging, set the `IMMICH_LOG_FORMAT` environment variable: + +```bash +IMMICH_LOG_FORMAT=json +``` + +:::tip +The default is `IMMICH_LOG_FORMAT=console` for human-readable logs with colors during development. For production deployments using log aggregation, use `IMMICH_LOG_FORMAT=json`. +::: + +### JSON Log Format + +When enabled, logs are output in structured JSON format: + +```json +{"level":"log","pid":36,"timestamp":1766533331507,"message":"Initialized websocket server","context":"WebsocketRepository"} +{"level":"warn","pid":48,"timestamp":1766533331629,"message":"Unable to open /build/www/index.html, skipping SSR.","context":"ApiService"} +{"level":"error","pid":36,"timestamp":1766533331690,"message":"Failed to load plugin immich-core:","context":"Error"} +``` + +This format includes: + +- `level`: Log level (log, warn, error, etc.) +- `pid`: Process ID +- `timestamp`: Unix timestamp in milliseconds +- `message`: Log message +- `context`: Service or component that generated the log + +For more information on log formats, see [`IMMICH_LOG_FORMAT`](/install/environment-variables.md#general). + [prom-file]: https://github.com/immich-app/immich/releases/latest/download/prometheus.yml diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index e9d95cf3fe..a7494d5415 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -34,6 +34,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N | `TZ` | Timezone | \*1 | server | microservices | | `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices | | `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices | +| `IMMICH_LOG_FORMAT` | Log output format (`console`, `json`) | `console` | server | api, microservices | | `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**\*2⚠️ | `/data` | server | api, microservices | | `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices | | `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | | diff --git a/server/src/dtos/env.dto.ts b/server/src/dtos/env.dto.ts index 22f3d4dd32..e088a33413 100644 --- a/server/src/dtos/env.dto.ts +++ b/server/src/dtos/env.dto.ts @@ -1,6 +1,6 @@ import { Transform, Type } from 'class-transformer'; import { IsEnum, IsInt, IsString, Matches } from 'class-validator'; -import { DatabaseSslMode, ImmichEnvironment, LogLevel } from 'src/enum'; +import { DatabaseSslMode, ImmichEnvironment, LogFormat, LogLevel } from 'src/enum'; import { IsIPRange, Optional, ValidateBoolean } from 'src/validation'; export class EnvDto { @@ -48,6 +48,10 @@ export class EnvDto { @Optional() IMMICH_LOG_LEVEL?: LogLevel; + @IsEnum(LogFormat) + @Optional() + IMMICH_LOG_FORMAT?: LogFormat; + @Optional() @Matches(/^\//, { message: 'IMMICH_MEDIA_LOCATION must be an absolute path' }) IMMICH_MEDIA_LOCATION?: string; diff --git a/server/src/enum.ts b/server/src/enum.ts index 9d0a2c0426..b150cdbfb3 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -454,6 +454,11 @@ export enum LogLevel { Fatal = 'fatal', } +export enum LogFormat { + Console = 'console', + Json = 'json', +} + export enum ApiCustomExtension { Permission = 'x-immich-permission', AdminOnly = 'x-immich-admin-only', diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index b87fcd2bb8..54a5d1987f 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -17,6 +17,7 @@ import { ImmichHeader, ImmichTelemetry, ImmichWorker, + LogFormat, LogLevel, QueueName, } from 'src/enum'; @@ -29,6 +30,7 @@ export interface EnvData { environment: ImmichEnvironment; configFile?: string; logLevel?: LogLevel; + logFormat?: LogFormat; buildMetadata: { build?: string; @@ -233,6 +235,7 @@ const getEnv = (): EnvData => { environment, configFile: dto.IMMICH_CONFIG_FILE, logLevel: dto.IMMICH_LOG_LEVEL, + logFormat: dto.IMMICH_LOG_FORMAT || LogFormat.Console, buildMetadata: { build: dto.IMMICH_BUILD, diff --git a/server/src/repositories/logging.repository.ts b/server/src/repositories/logging.repository.ts index 576ee6c810..39867b14d0 100644 --- a/server/src/repositories/logging.repository.ts +++ b/server/src/repositories/logging.repository.ts @@ -2,7 +2,7 @@ import { ConsoleLogger, Inject, Injectable, Scope } from '@nestjs/common'; import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-enabled.util'; import { ClsService } from 'nestjs-cls'; import { Telemetry } from 'src/decorators'; -import { LogLevel } from 'src/enum'; +import { LogFormat, LogLevel } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; type LogDetails = any; @@ -27,10 +27,12 @@ export class MyConsoleLogger extends ConsoleLogger { constructor( private cls: ClsService | undefined, - options?: { color?: boolean; context?: string }, + options?: { json?: boolean; color?: boolean; context?: string }, ) { - super(options?.context || MyConsoleLogger.name); - this.isColorEnabled = options?.color || false; + super(options?.context || MyConsoleLogger.name, { + json: options?.json ?? false, + }); + this.isColorEnabled = !options?.json && (options?.color || false); } isLevelEnabled(level: LogLevel) { @@ -79,10 +81,17 @@ export class LoggingRepository { @Inject(ConfigRepository) configRepository: ConfigRepository | undefined, ) { let noColor = false; + let logFormat = LogFormat.Console; if (configRepository) { - noColor = configRepository.getEnv().noColor; + const env = configRepository.getEnv(); + noColor = env.noColor; + logFormat = env.logFormat ?? logFormat; } - this.logger = new MyConsoleLogger(cls, { context: LoggingRepository.name, color: !noColor }); + this.logger = new MyConsoleLogger(cls, { + context: LoggingRepository.name, + json: logFormat === LogFormat.Json, + color: !noColor, + }); } static create(context?: string) { diff --git a/server/test/repositories/config.repository.mock.ts b/server/test/repositories/config.repository.mock.ts index 01e724529c..62e498372e 100644 --- a/server/test/repositories/config.repository.mock.ts +++ b/server/test/repositories/config.repository.mock.ts @@ -1,4 +1,4 @@ -import { DatabaseExtension, ImmichEnvironment, ImmichWorker } from 'src/enum'; +import { DatabaseExtension, ImmichEnvironment, ImmichWorker, LogFormat } from 'src/enum'; import { ConfigRepository, EnvData } from 'src/repositories/config.repository'; import { RepositoryInterface } from 'src/types'; import { Mocked, vitest } from 'vitest'; @@ -6,6 +6,7 @@ import { Mocked, vitest } from 'vitest'; const envData: EnvData = { port: 2283, environment: ImmichEnvironment.Production, + logFormat: LogFormat.Console, buildMetadata: {}, bull: {