axon_shelf 0.1.0 copy "axon_shelf: ^0.1.0" to clipboard
axon_shelf: ^0.1.0 copied to clipboard

Shelf middleware adapter for Axon: HTTP request logging for Dart servers. Wraps axon_core's token engine and serializers into a standard Shelf Middleware.

axon_shelf #

pub package style: meragix

Shelf middleware adapter for Axon: HTTP request logging for Dart servers.


Installation #

dependencies:
  axon_shelf: ^0.1.0

Quick start #

import 'package:axon_shelf/axon_shelf.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;

void main() async {
  final handler = Pipeline()
      .addMiddleware(axonShelf())
      .addHandler((req) => Response.ok('hello'));

  final server = await io.serve(handler, 'localhost', 8080);
  print('Listening on port ${server.port}');
}

Output (colored in a terminal, plain in pipes/log files):

GET / 200 3 ms
POST /api/users 201 12 ms
GET /api/users/99 404 1 ms

Configuration #

All options are set via AxonShelfConfig:

axonShelf(config: AxonShelfConfig(
  format: LogFormats.dev,           // format string: compiled once at startup
  serializer: null,                 // null = auto (ColoredText or Text based on TTY)
  sink: print,                      // log destination
  bodyCapture: BodyCaptureConfig.disabled,
  strict: true,                     // throw FormatCompilationError on unknown tokens
  skip: null,                       // SkipPredicate?: suppress log lines
  immediate: false,                 // log before handler runs
  colorMode: ColorMode.auto,        // auto | always | never
  userExtractor: null,              // UserExtractor?: for :remote-user token
))

Format strings #

// Predefined
axonShelf(config: AxonShelfConfig(format: LogFormats.dev))
axonShelf(config: AxonShelfConfig(format: LogFormats.combined))
axonShelf(config: AxonShelfConfig(format: LogFormats.common))
axonShelf(config: AxonShelfConfig(format: LogFormats.short))
axonShelf(config: AxonShelfConfig(format: LogFormats.tiny))

// Custom
axonShelf(config: AxonShelfConfig(
  format: ':remote-addr :method :url :status :response-time.ms ms',
))
// => 203.0.113.42 GET /api/users 200 3.21 ms

See axon_core for the full token reference.


Serializers #

Text (default) #

Auto-selected based on TTY detection. Colored in terminals, plain in pipes.

// Force colors (CI with ANSI support)
axonShelf(config: AxonShelfConfig(colorMode: ColorMode.always))

// Force plain (log files, systemd journal)
axonShelf(config: AxonShelfConfig(colorMode: ColorMode.never))

JSON - ELK / Datadog / Loki #

Numeric types preserved: status is int, responseTime is int/double:

axonShelf(config: AxonShelfConfig(
  serializer: JsonSerializer(
    extraFields: {'service': 'my-api', 'env': 'production'},
  ),
  sink: myStructuredLogger.info,
))
// => {"ts":"2026-04-03T10:00:00.000Z","method":"GET","status":200,"responseTime":3,"service":"my-api"}

Filtering with skip #

// Production: log only errors
axonShelf(config: AxonShelfConfig(
  skip: SkipPredicates.successfulRequests,
))

// Suppress health check / readiness probe noise
axonShelf(config: AxonShelfConfig(
  skip: SkipPredicates.paths({'/health', '/ready', '/ping'}),
))

// Skip fast successful responses
axonShelf(config: AxonShelfConfig(
  skip: SkipPredicates.fasterThan(Duration(milliseconds: 100)),
))

// Custom predicate: full LogContext access
axonShelf(config: AxonShelfConfig(
  skip: (ctx) =>
      ctx.request.method == 'OPTIONS' ||
      ctx.request.path.startsWith('/assets/'),
))

// Combine: log errors OR slow requests
axonShelf(config: AxonShelfConfig(
  skip: (ctx) {
    final status = ctx.response?.statusCode ?? 0;
    final ms = ctx.response?.latency.inMilliseconds ?? 0;
    return status < 400 && ms < 500;
  },
))

Immediate mode #

Log at request arrival: before the handler runs. Useful when the server may crash before sending a response.

axonShelf(config: AxonShelfConfig(immediate: true))

Response fields (:status, :response-time, :content-length) render as - in immediate mode. The skip predicate receives a null ResponseSnapshot, predicates that inspect the response fall back to their null-safe defaults.


Color output #

colorMode Behavior
ColorMode.auto Colored if stdout.hasTerminal, plain otherwise (default)
ColorMode.always Always emit ANSI codes
ColorMode.never Plain text only

Status colors: 2xx green · 3xx cyan · 4xx yellow · 5xx red
Latency colors: < 100ms green · < 500ms yellow · ≥ 500ms red


Header masking #

Apply masking at the snapshot level before the token engine sees the data:

// In ShelfSnapshotFactory, wrap headers with MaskedHeaderView
final masked = MaskedHeaderView(
  ShelfHeaderView.fromRequest(request),
  [MaskingRules.bearerToken, MaskingRules.cookie],
);

Or use a custom masking rule:

MaskingRule redactApiKey = (key, value) =>
    key == 'x-api-key' ? '[REDACTED]' : null;

Body capture #

Opt-in, non-blocking stream view, the handler always receives the full unmodified stream:

axonShelf(config: AxonShelfConfig(
  bodyCapture: BodyCaptureConfig(maxRequestBodyBytes: 2048),
))

If content-length > maxRequestBodyBytes, the captured body is truncated and the remainder passes through untouched. Body capture is disabled by default: enabling it on large uploads (multipart, binary) will truncate at the limit.


Authenticated user — :remote-user #

:remote-user is auth-system agnostic. You provide a UserExtractor that receives the full LogContext:

// From request header (API key, reverse proxy injection)
axonShelf(config: AxonShelfConfig(
  userExtractor: (ctx) => ctx.request.headers['x-user-id'],
))

// From Shelf context (JWT middleware, shelf_auth, custom auth)
// Import ShelfRequestSnapshotX to access the Shelf request context
import 'package:axon_shelf/axon_shelf.dart';

axonShelf(config: AxonShelfConfig(
  userExtractor: (ctx) =>
      ctx.request.shelfContext?['user'] as String?,
))

// From response header (auto-login or auth gateway pattern)
axonShelf(config: AxonShelfConfig(
  userExtractor: (ctx) =>
      ctx.response?.headers['x-authenticated-as'],
))

Auth middleware example, inject user into Shelf context:

Handler jwtMiddleware(Handler inner) => (request) async {
  final token = request.headers['authorization']?.replaceFirst('Bearer ', '');
  final user = token != null ? await jwtService.verify(token) : null;
  return inner(
    request.change(context: {'user': user?.id}),
  );
};

// Wire both middlewares
Pipeline()
  .addMiddleware(jwtMiddleware)
  .addMiddleware(axonShelf(config: AxonShelfConfig(
    format: LogFormats.combined,  // includes :remote-user
    userExtractor: (ctx) =>
        ctx.request.shelfContext?['user'] as String?,
  )))
  .addHandler(myHandler);

// Output:
// 203.0.113.42 - john_doe [03/Apr/2026:10:00:00 +0000] "GET /api/orders" 200 1024

Custom tokens #

final registry = TokenRegistry();
registerBuiltInTokens(registry);
registerShelfTokens(registry);  // adds :remote-addr, :referrer, :user-agent

// Register a custom token
registry.define('request-id', (ctx) => TokenOutput(
  key: 'requestId',
  value: ctx.request.headers['x-request-id'],
));

// Compile with the extended registry
final format = FormatCompiler(registry)
    .compile(':method :url :status [:request-id] :response-time ms');

// Pass a pre-built serializer to axonShelf
axonShelf(config: AxonShelfConfig(
  serializer: TextSerializer(compiledFormat: format),
))

Shelf-specific tokens #

These tokens are registered automatically by axonShelf() and are not available in axon_core:

Token Description Notes
:remote-addr Client IP Prefers X-Forwarded-For
:referrer Referer / Referrer header Both spellings supported
:user-agent User-Agent header
:remote-user Authenticated user identity Via UserExtractor

Error handling #

axonShelf logs even when the handler throws. A synthetic 500 is recorded, the original error is re-thrown with its stack trace preserved:

// Handler throws → log written with status 500 → error propagates

Multiple middleware instances #

Shelf supports stacking multiple axonShelf instances, useful for routing logs to different destinations:

Pipeline()
  // Log all requests to structured JSON sink
  .addMiddleware(axonShelf(config: AxonShelfConfig(
    serializer: JsonSerializer(),
    sink: datadogLogger.info,
  )))
  // Also log errors to console with colors
  .addMiddleware(axonShelf(config: AxonShelfConfig(
    skip: SkipPredicates.successfulRequests,
    colorMode: ColorMode.always,
  )))
  .addHandler(myHandler);

License #

MIT — see LICENSE.

Part of the Axon ecosystem by Meragix.

0
likes
160
points
36
downloads

Documentation

API reference

Publisher

verified publishermeragix.dev

Weekly Downloads

Shelf middleware adapter for Axon: HTTP request logging for Dart servers. Wraps axon_core's token engine and serializers into a standard Shelf Middleware.

Homepage
Repository (GitHub)
View/report issues

Topics

#logging #http-logger #shelf #server-side-dart

License

MIT (license)

Dependencies

axon_core, shelf

More

Packages that depend on axon_shelf