axon_dart_frog 0.1.0
axon_dart_frog: ^0.1.0 copied to clipboard
Dart Frog middleware adapter for Axon: HTTP request logging for Dart servers. Wraps axon_core's token engine into idiomatic Dart Frog middleware using handler.use() and RequestContext.
axon_dart_frog #
Dart Frog middleware adapter for Axon: HTTP request logging for Dart servers.
Installation #
dependencies:
axon_dart_frog: ^0.1.0
Quick start #
In routes/_middleware.dart:
import 'package:dart_frog/dart_frog.dart';
import 'package:axon_dart_frog/axon_dart_frog.dart';
Handler middleware(Handler handler) {
return handler.use(axonDartFrog());
}
Output:
GET /api/users 200 3 ms
POST /api/orders 201 12 ms
GET /api/users/99 404 1 ms
⚠️ Middleware order — read this first #
Dart Frog wraps middlewares like an onion. handler.use(A).use(B) means B is the outer laye: it runs last on the request phase and first on the response phase.
handler.use(A).use(B).use(C)
Request phase: C → B → A → handler
Response phase: handler → A → B → C
Axon must always be the outermost layer (.use() called last) so it runs after all auth/session middlewares. This guarantees that DI values injected by inner middlewares are available via context.read<T>() when UserExtractor runs.
// ✅ CORRECT: Axon is outermost, auth runs first on request
Handler middleware(Handler handler) {
return handler
.use(authMiddleware()) // inner: injects user, runs first
.use(axonDartFrog(...)); // outer: reads user, runs last
}
// ❌ WRONG: Axon runs before auth, context.read<User>() returns null
Handler middleware(Handler handler) {
return handler
.use(axonDartFrog(...)) // outer: runs first, DI not yet populated
.use(authMiddleware()); // inner: too late
}
Configuration #
handler.use(axonDartFrog(
config: AxonDartFrogConfig(
format: LogFormats.dev, // compiled once at startup
serializer: null, // null = auto (ColoredText or Text)
sink: print,
strict: true,
skip: null,
immediate: false,
colorMode: ColorMode.auto, // auto | always | never
userExtractor: null,
),
))
Format strings #
Same as axon_shelf: all tokens from axon_core plus Dart Frog-specific ones.
// Predefined
handler.use(axonDartFrog(config: AxonDartFrogConfig(format: LogFormats.combined)))
// Custom
handler.use(axonDartFrog(config: AxonDartFrogConfig(
format: ':remote-addr :method :url :status :response-time.ms ms',
)))
Filtering with skip #
// Log only errors
handler.use(axonDartFrog(config: AxonDartFrogConfig(
skip: SkipPredicates.successfulRequests,
)))
// Suppress health checks
handler.use(axonDartFrog(config: AxonDartFrogConfig(
skip: SkipPredicates.paths({'/health', '/ready', '/ping'}),
)))
// Custom
handler.use(axonDartFrog(config: AxonDartFrogConfig(
skip: (ctx) => ctx.request.method == 'OPTIONS',
)))
Serializers #
Text (default, auto-colored) #
handler.use(axonDartFrog(config: AxonDartFrogConfig(
colorMode: ColorMode.always, // force colors in CI
)))
JSON - ELK / Datadog / Loki #
handler.use(axonDartFrog(config: AxonDartFrogConfig(
serializer: JsonSerializer(
extraFields: {'service': 'my-api', 'env': 'production'},
),
sink: myStructuredLogger.info,
)))
Immediate mode #
Log at request arrival before the handler runs:
handler.use(axonDartFrog(config: AxonDartFrogConfig(immediate: true)))
Response fields (:status, :response-time, :content-length) render as -. Useful when the server may crash before sending a response.
Authenticated user: :remote-user #
Dart Frog's DI system (context.read<T>()) is the idiomatic way to pass the authenticated user from an auth middleware to the logger. Use DartFrogRequestSnapshotX to access the RequestContext inside the UserExtractor:
Handler middleware(Handler handler) {
return handler
// 1. Auth middleware provides the user
.use(_jwtMiddleware())
// 2. Axon reads it via DI: order matters
.use(axonDartFrog(
config: AxonDartFrogConfig(
format: ':remote-addr - :remote-user [:date.clf] ":method :url" :status',
skip: SkipPredicates.paths({'/health'}),
userExtractor: (logCtx) {
final reqCtx = logCtx.request.dartFrogContext;
return reqCtx?.read<AuthUser?>()?.id;
},
),
));
}
Auth middleware example:
Middleware _jwtMiddleware() {
return (Handler handler) {
return (RequestContext context) async {
final token = context.request.headers['authorization']
?.replaceFirst('Bearer ', '');
final user = token != null ? await verifyJwt(token) : null;
return handler(context.provide<AuthUser?>(() => user));
};
};
}
Output with authenticated user:
203.0.113.42 - john_doe [03/Apr/2026:10:00:00 +0000] "GET /api/orders" 200
203.0.113.42 - - [03/Apr/2026:10:00:00 +0000] "GET /api/public" 200
Other UserExtractor patterns #
// From request header (API key, reverse proxy injection)
userExtractor: (logCtx) => logCtx.request.headers['x-user-id'],
// From response header (auth gateway pattern)
userExtractor: (logCtx) => logCtx.response?.headers['x-authenticated-as'],
// Dart Frog auth package (dart_frog_auth)
userExtractor: (logCtx) =>
logCtx.request.dartFrogContext?.read<User?>()?.email,
Scoped logging per route directory #
Dart Frog supports per-directory middleware. Use this to apply different log configs to different route groups:
routes/
├── _middleware.dart ← global: combined format, JSON to Datadog
├── api/
│ └── _middleware.dart ← API routes: errors only
└── admin/
└── _middleware.dart ← Admin routes: full CLF + user identity
// routes/api/_middleware.dart
Handler middleware(Handler handler) {
return handler.use(axonDartFrog(
config: AxonDartFrogConfig(
skip: SkipPredicates.successfulRequests,
),
));
}
// routes/admin/_middleware.dart
Handler middleware(Handler handler) {
return handler.use(axonDartFrog(
config: AxonDartFrogConfig(
format: LogFormats.combined,
userExtractor: (logCtx) =>
logCtx.request.dartFrogContext?.read<AdminUser?>()?.username,
),
));
}
Dart Frog-specific tokens #
Registered automatically by axonDartFrog():
| Token | Description |
|---|---|
:remote-addr |
Client IP from X-Forwarded-For or X-Real-IP |
:referrer |
Referer / Referrer header |
:user-agent |
User-Agent header |
:remote-user |
Via UserExtractor — reads from RequestContext DI |
Dart Frog vs Shelf #
axon_shelf |
axon_dart_frog |
|
|---|---|---|
| Handler type | Request → Response |
RequestContext → Response |
| DI access | Via ShelfRequestSnapshotX.shelfContext |
Via DartFrogRequestSnapshotX.dartFrogContext |
| User injection | request.change(context: {'user': ...}) |
context.provide<User>(() => user) |
:remote-addr |
From shelf.io.connection_info |
From X-Forwarded-For / X-Real-IP |
Error handling #
axonDartFrog logs even when the handler throws. A synthetic 500 is recorded and the original error is re-thrown with its stack trace preserved.
License #
MIT — see LICENSE.