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: {