Skip to content

Structured logging

You’ll configure production-ready logging: JSON lines on stdout, an access log without bodies, your own log lines tied to the request id, and a custom logger that redacts secrets.

  • A gisila project.
import 'dart:io';
import 'package:gisila/gisila.dart';
final prod = Platform.environment['APP_ENV'] == 'production';
final app = GisilaApp(
config: AppConfig(
logLevel: prod ? LogLevel.info : LogLevel.debug,
logFormat: prod ? LogFormat.json : LogFormat.text,
),
);

ConsoleLogger writes info and below to stdout and warning and above to stderr. Container platforms (including the gisila panel) collect both.

One line per request is on by default (logRequests: true), at info, with method, path, query, status, duration, remote address, request id and principal id. Bodies are off:

AppConfig(
logRequests: true,
logRequestBody: false, // never in production: passwords, tokens
logResponseBody: false,
)

When enabled, bodies are truncated to 2048 characters; streams and server-sent events are never read.

Use app.config.logger so your lines share the format:

class OrderService extends Service {
Logger get _log => GisilaApp.maybeOf(ctx.request)!.config.logger;
Future<Order> place(OrderForm form) async {
final order = await _repo.insert(form);
_log.info('order placed', fields: {
'request_id': ctx.requestId,
'user_id': ctx.principal?.id,
'order_id': order.id,
'total_cents': order.totalCents,
});
return order;
}
}

The convenience methods (info, warning, …) don’t attach the request id on their own, so pass it as a field, or build a LogEvent with requestId: and call logger.log(...). Clients can send their own X-Request-Id (1 to 128 safe characters) to correlate across services; it is echoed back on the response.

Keep field values small and never log secrets or full request bodies.

Implement Logger to send logs elsewhere or to transform them. This one redacts sensitive fields and forwards to the console:

class RedactingLogger extends Logger {
RedactingLogger(this.inner);
final Logger inner;
static final _sensitive = RegExp(r'pass|token|secret|authorization', caseSensitive: false);
@override
LogLevel get level => inner.level;
@override
void log(LogEvent e) => inner.log(LogEvent(
level: e.level,
message: e.message,
timestamp: e.timestamp,
requestId: e.requestId,
principalId: e.principalId,
error: e.error,
stackTrace: e.stackTrace,
fields: {
for (final f in e.fields.entries)
f.key: _sensitive.hasMatch(f.key) ? '[redacted]' : f.value,
},
));
}
final app = GisilaApp(
config: AppConfig(logger: RedactingLogger(ConsoleLogger(format: LogFormat.json))),
);

When you pass logger, logLevel and logFormat are ignored; configure them on the logger you pass.

Terminal window
APP_ENV=production dart run bin/server.dart &
curl -s localhost:8080/orders -H 'x-request-id: demo-123' > /dev/null
{"method":"GET","path":"/orders","query":"","status":401,"duration_ms":1,"remote":"127.0.0.1:53422","ts":"2026-10-31T09:00:00.000Z","level":"info","msg":"GET /orders -> 401 (1ms)","requestId":"demo-123"}

In tests, capture lines with ConsoleLogger(stdoutSink: lines.add, stderrSink: lines.add).