carapacik_dio_logger 2.0.0
carapacik_dio_logger: ^2.0.0 copied to clipboard
Dio logger with normal and compact modes, bounded redacted events, safe console output and cURL export.
carapacik_dio_logger #
A portable Dio interceptor with normal and compact modes, bounded redacted events, Unicode-safe console output, and optional POSIX cURL export.
dependencies:
carapacik_dio_logger: ^2.0.0
Requires Dio ^5.11.0 (including HTTP QUERY support). No Flutter or filesystem dependency.
Request/response mode and body format #
import 'package:carapacik_dio_logger/carapacik_dio_logger.dart';
import 'package:dio/dio.dart';
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.test'));
dio.interceptors.add(CarapacikDioLogger());
mode controls request/response details: normal prints blocks with enabled
sections, while compact prints one completion summary per observed attempt.
Compact summaries exclude headers, query parameters, request options and cURL
sections.
Body output has independent controls:
requestBody,responseBodyanderrorBodyenable the corresponding body.compactBody: false(default) keeps readable multiline previews.compactBody: trueprints each enabled body on one line.
Every body preview is a separate event/message in either mode. It is never
embedded in the request/response detail block or completion summary. An enabled
cURL section may also contain its own redacted representation of the request body.
compactBody does not enable bodies by itself. Disabled bodies and null data produce no body
message; empty strings, maps and lists are still valid bodies.
mode |
Body enabled | compactBody |
Output |
|---|---|---|---|
normal |
No | Either | Request/response blocks only |
normal |
Yes | false |
Blocks + separate multiline bodies |
normal |
Yes | true |
Blocks + separate single-line bodies |
compact |
No | Either | Completion summary only |
compact |
Yes | false |
Completion summary + separate multiline bodies |
compact |
Yes | true |
Completion summary + separate single-line bodies |
Headers and bodies default to enabled in normal mode; bodies default to off in compact mode. cURL and stack traces are opt-in. Redaction applies only to explicitly configured fields and headers.
Compact summaries without bodies:
CarapacikDioLogger(
mode: DioLogMode.compact,
requestBody: false,
responseBody: false,
errorBody: false,
);
[HTTP] GET api.example.test/items → 200 124ms
[HTTP] POST api.example.test/items → 503 42ms badResponse
[HTTP] GET api.example.test/items → - 20001ms connectionTimeout
Compact summaries with a multiline body:
CarapacikDioLogger(
mode: DioLogMode.compact,
responseBody: true,
compactBody: false,
);
The response and its body are emitted in order as separate messages.
compactBody only changes the contents of the body message.
Message 1 — response:
[HTTP] GET api.example.test/items → 200 124ms
Message 2 — body (compactBody: false):
[HTTP] GET api.example.test/items → 200 124ms [responseBody]
{
"ok": true
}
With compactBody: true, the body is still the next, separate message.
Only its JSON formatting changes:
Message 1 — response:
[HTTP] GET api.example.test/items → 200 124ms
Message 2 — body (compactBody: true):
[HTTP] GET api.example.test/items → 200 124ms [responseBody] {"ok":true}
The opposite combination works too:
CarapacikDioLogger(
mode: DioLogMode.normal,
compactBody: true,
requestBody: true,
responseBody: true,
);
This prints normal detail blocks and separate single-line body messages.
JSON compaction preserves whitespace inside string values. Plain-text line
feeds and tabs become \n and \t; other control characters remain escaped,
and omission markers remain visible.
errorBody inherits responseBody unless explicitly set. response: false
disables accepted response events and their bodies; error: false disables
failure events and their bodies. Body flags work in either mode.
Interceptor constructor #
All constructor parameters are shown below with their defaults. A null
requestBody or responseBody is resolved from mode; a null errorBody
inherits the resolved responseBody. limits: null creates DioLogLimits().
CarapacikDioLogger(
mode: DioLogMode.normal,
compactBody: false,
request: true,
requestHeader: true,
requestQueryParameters: true,
requestBody: null, // normal: true; compact: false
requestExtra: false,
extraKeys: const {},
response: true,
responseHeader: true,
responseBody: null, // normal: true; compact: false
error: true,
errorBody: null, // inherits responseBody
errorMessage: true,
errorStackTrace: false,
convertFormData: true,
showCurl: false,
showLog: true,
showRedirects: false,
showResponseMetadata: false,
label: 'HTTP',
context: null,
redaction: const DioLogRedaction(),
limits: null,
filter: null,
responseFilter: null,
errorFilter: null,
levelResolver: null,
onLog: null,
colorized: false,
chunkSize: 1024,
wrapWidth: null,
logPrint: print,
);
Mode and request output #
| Parameter | Default | What is printed |
|---|---|---|
mode |
DioLogMode.normal |
normal: request and completion blocks with enabled detail sections. compact: completion line. Does not select body formatting. |
compactBody |
false |
true: a separate single-line message for each enabled body. false: a separate multiline message. Independent of mode; does not enable bodies. |
request |
true |
Normal Options section: response type, content type, timeouts, redirects and connection settings. false removes this section only; other request flags are independent. No effect on compact summaries. |
requestHeader |
true |
Normal Request headers section and cURL -H arguments, with explicitly selected values redacted. No compact header section. |
requestQueryParameters |
true |
Normal Query section and query in the displayed URL/cURL; false removes these query details. Compact endpoints always omit queries. |
requestBody |
null → normal true, compact false |
Separate [requestBody] message in either mode; also controls the cURL payload. compactBody selects multiline or single-line format. No message for null data. |
requestExtra |
false |
Normal Extra section when enabled with a nonempty extraKeys allowlist. No compact section. |
extraKeys |
const {} |
Names allowed in Extra, e.g. {'operation'} prints {"operation":"create-item"} when that key exists. Other keys are omitted. |
Response and error output #
| Parameter | Default | What is printed |
|---|---|---|
response |
true |
Accepted response completion and optional body. false suppresses both in either mode. Acceptance follows Dio's validateStatus, not a hard-coded status range. |
responseHeader |
true |
Normal Response headers section for accepted responses and failures with a response. No compact header section. |
responseBody |
null → normal true, compact false |
Separate [responseBody] message in either mode, if data is non-null. Format follows compactBody. |
error |
true |
Failure completion and optional error body. false suppresses both in either mode. |
errorBody |
null → resolved responseBody |
Separate [errorBody] message in either mode, formatted by compactBody. Requires a failure with non-null response data. Can be enabled independently of responseBody. |
errorMessage |
true |
Normal Error section with the sanitized Dio message. Compact always keeps the failure type, e.g. connectionTimeout, without dumping the message. |
errorStackTrace |
false |
Normal Stack trace section when enabled, bounded by string limits. No compact stack trace. |
showRedirects |
false |
Normal Redirects section, e.g. ["302 GET https://api.example.test/items"], when the adapter provides redirects. |
showResponseMetadata |
false |
Normal Response metadata section with httpVersion, declaredBytes, realUri and isRedirect. Compact text omits this section; events still carry available protocol and declared size fields. |
Shared controls and cURL #
| Parameter | Default | What is printed |
|---|---|---|
convertFormData |
true |
With cURL body output enabled, includes multipart text via --form-string. false prints # Multipart payload omitted. instead. Body previews still show bounded multipart metadata; uploads are never read. |
showCurl |
false |
Normal cURL (POSIX shell) section. No effect on compact text. Explicit toCurl() remains available in either mode. |
showLog |
true |
false disables interceptor output, callbacks and timing entirely. It does not disable an explicit toCurl() call. |
label |
'HTTP' |
Prefix in every summary, e.g. label: 'API' prints [API] GET ...; normal block headings also use this label. |
context |
null |
Application fields appended to every summary/body message, e.g. operation=create-item. Null adds nothing; values are bounded and optionally redacted. |
redaction |
const DioLogRedaction() |
Empty hidden-key sets by default. Replaces configured secrets with [REDACTED], e.g. {"password":"[REDACTED]"}; custom URI/text sanitizers transform displayed values. |
limits |
null → DioLogLimits() |
Bounds previews; omitted content gets markers such as … [truncated], [max depth] or [entries omitted]. See the limit values below. |
filter |
null |
(request, phase) => false suppresses that phase before serialization. Body events have their own phases and are filtered separately. |
responseFilter |
null |
Returning false suppresses an accepted response and its body; request events are unaffected. |
errorFilter |
null |
Returning false suppresses a failure and its body; request events are unaffected. |
levelResolver |
null → defaultLevel |
Changes completion event severity and console color, not summary wording. Request events remain debug. For example, returning DioLogLevel.warning colors completions yellow when colors are enabled. |
Output destination and console layout #
| Parameter | Default | What is printed |
|---|---|---|
onLog |
null |
When supplied, receives DioLogEvent instead of built-in console output. Use DioLogFormatter for text or event.toJson() for storage. Raw event sections retain their readable previews; configure DioLogFormatter(compactBody: true) to minify bodies. |
colorized |
false |
ANSI console colors: debug cyan, info green, warning yellow, error red. Does not affect onLog events. |
chunkSize |
1024 |
Maximum UTF-8 bytes per logPrint call, including ANSI (minimum 32). Long messages may be split into several calls; this does not truncate the logical event. |
wrapWidth |
null |
Null adds no word wrapping. A value such as 80 wraps at spaces with indentation (minimum 20), including compact body lines. Long tokens are left to byte chunking. |
logPrint |
print |
Receives formatted console chunks. For example, debugPrint uses Flutter's throttling. Not invoked when onLog is supplied. |
Output examples #
Normal mode with request options, headers and query details disabled:
CarapacikDioLogger(
request: false,
requestHeader: false,
requestQueryParameters: false,
responseHeader: false,
);
For a POST body {'name': 'Example'} and a 201 response {'ok': true},
four separate messages are emitted:
Message 1 — request:
┏ HTTP REQUEST
[HTTP] POST api.example.test/items
URL: https://api.example.test/items
┗━━━━━━━━━━━━━━━━━━━━
Message 2 — request body:
[HTTP] POST api.example.test/items [requestBody]
{
"name": "Example"
}
Message 3 — response:
┏ HTTP RESPONSE
[HTTP] POST api.example.test/items → 201 16ms
URL: https://api.example.test/items
┗━━━━━━━━━━━━━━━━━━━━
Message 4 — response body:
[HTTP] POST api.example.test/items → 201 16ms [responseBody]
{
"ok": true
}
With normal defaults, Request headers, Options and Response headers
sections are also included; Query is included if the URL has a query.
Optional sections appear only when enabled and applicable data exists.
The same exchange with compact summaries, compact bodies and application context:
CarapacikDioLogger(
mode: DioLogMode.compact,
requestBody: true,
responseBody: true,
compactBody: true,
context: (_) => {'operation': 'create-item'},
);
Request body message:
[HTTP] POST api.example.test/items operation=create-item [requestBody] {"name":"Example"}
Response message:
[HTTP] POST api.example.test/items → 201 16ms operation=create-item
Next message — response body:
[HTTP] POST api.example.test/items → 201 16ms operation=create-item [responseBody] {"ok":true}
In compact mode, an HTTP failure with errorBody: true and compactBody: true
prints a completion message and a separate body message:
Error message:
[HTTP] GET api.example.test/items → 503 42ms badResponse
Next message — error body:
[HTTP] GET api.example.test/items → 503 42ms badResponse [errorBody] {"message":"Unavailable"}
Elapsed times are illustrative. Network failures with no response have no
response body. showLog: false produces no output in either mode.
Application context and filtering #
The package does not assign special meaning to application headers. Add your
own bounded fields through context:
CarapacikDioLogger(
mode: DioLogMode.compact,
context: (request) => {'operation': request.extra['operation'] ?? '-'},
filter: (request, phase) => !request.path.endsWith('/health'),
responseFilter: (response) => response.statusCode != 304,
errorFilter: (error) => error.type != DioExceptionType.cancel,
);
Context is repeated on any body event. Extra data is not dumped by default:
normal mode requires both requestExtra: true and an extraKeys allowlist.
Filters run before body preview/serialization. Callbacks must be fast.
Default severity: request events and cancellations use debug; 5xx completions
use error; other Dio failures and 4xx completions use warning; remaining
completions use info. levelResolver can replace
the completion policy, including for statuses accepted by validateStatus.
Redaction and limits #
Nothing is masked by default: hiddenHeaders and hiddenFields are empty.
Authorization, Cookie, passwords and tokens remain visible when their sections
are enabled. Choose exactly what to hide:
CarapacikDioLogger(
redaction: const DioLogRedaction(
hiddenHeaders: {'authorization', 'cookie', 'set-cookie'},
hiddenFields: {'password', 'token', 'access_token'},
),
limits: DioLogLimits(
maxBodyBytes: 8192,
maxStringLength: 1024,
maxCollectionEntries: 30,
maxDepth: 5,
maxEndpointLength: 160,
),
);
Matching ignores case and separators. Masked values become [REDACTED];
network objects and the actual request are never changed. The same configured
policy applies to request/response/error previews and cURL. There are no
implicitly added hidden keys. Limits and control-character escaping still apply
with empty hidden-key sets; compact mode still shows only its selected summary.
Use sanitizeUri for application-specific secrets in paths and sanitizeText
for secrets in plain text/error messages. The callback receives complete input
before escaping or whitespace folding. If the input exceeds its preview limit,
the entire value is omitted without calling the callback; this avoids revealing
a secret whose closing delimiter would be truncated. Callback output is escaped
and bounded as usual. Arbitrary prose cannot be redacted
by structured field names. URI credentials and fragments are retained in
detailed URIs by default; remove them explicitly with
sanitizeUri: (uri) => uri.replace(userInfo: '', fragment: '').
The sanitizer escapes control codes and bounds traversal before serialization. Cycles,
unknown objects, binary payloads and streams get a short description; streams
are never consumed and multipart uploads are never finalized for logging.
Repeated multipart fields remain separate entries.
Console and file output #
logPrint defaults to Dart’s print function. In Flutter, pass logPrint: debugPrint to use
Flutter's throttled output. Set showLog explicitly for your build policy;
the package does not infer debug/release mode.
wrapWidth is an optional readability hint. chunkSize is a separate UTF-8
transport limit (default 1024, minimum 32 bytes), not a payload limit. Chunking
preserves Unicode scalar values and each colored chunk owns its ANSI reset.
The console/IDE may still wrap lines using its own width. A body is one logical
event even when the console must split its transport output.
To deliver structured events instead of console output, provide onLog:
final formatter = DioLogFormatter(
mode: DioLogMode.compact,
compactBody: true,
);
final interceptor = CarapacikDioLogger(
mode: DioLogMode.compact,
onLog: (event) {
final text = formatter.format(event); // no timestamp/level duplication
// Forward text and event.level to your application's logger.
},
);
onLog replaces built-in printing. Configure your own DioLogFormatter with
the desired mode and compactBody. The interceptor’s mode determines which
details are collected; its compactBody only affects built-in console formatting.
Each event is a bounded snapshot containing the timestamp, phase, level, endpoint,
status, elapsed time, optional response metadata, application context and sections.
It does not retain Response or RequestOptions objects.
jsonEncode(event.toJson()) produces a JSONL record with no ANSI or console
wrapping. Use one record per event; do not store console chunks as records.
On platforms that support dart:io, an application can write events to an IOSink:
final sink = File('http.jsonl').openWrite(mode: FileMode.append);
final logger = CarapacikDioLogger(
mode: DioLogMode.compact,
onLog: (event) => sink.writeln(jsonEncode(event.toJson())),
);
// Observe sink.done errors in your application; on shutdown:
await sink.flush();
await sink.close();
The example requires dart:io and dart:convert imports in the application.
Directory selection, disk errors, rotation, retention and shutdown belong to the
output owner. The interceptor does not block requests on asynchronous outputs. Sync
callback failures and asynchronous onLog failures are contained so logging
cannot replace a network result.
cURL #
Enable showCurl: true in normal mode, or call logger.toCurl(requestOptions)
to export without console wrapping or ANSI. Commands target POSIX shells, not
PowerShell/cmd. Quoting handles single quotes, dollar signs and backticks.
Request header/body/query flags also govern cURL. If redaction is configured,
an export may need credentials restored before replay.
Multipart text uses --form-string. Upload files are explicitly omitted:
MultipartFile.filename does not establish a local file path. Streams and
payloads outside preview limits are omitted with a comment; the package does
not emit a truncated command as if it were complete. HTTP methods are not
restricted to GET/POST/PUT; QUERY and other methods may carry bodies.
Supported body types #
HTTP content types and Dart runtime types are different layers. This logger
previews the data available to the interceptor and does not decode every MIME
format itself. The following examples show body text. Every body has a separate
message with a [requestBody], [responseBody] or [errorBody] prefix.
compactBody controls its layout independently of request/response mode.
Examples containing [REDACTED] assume the corresponding hidden keys were configured.
| Data observed by the interceptor | compactBody: false |
compactBody: true / behavior |
|---|---|---|
JSON Map or List, including nested values |
Indented, redacted JSON | {"items":[1,2],"password":"[REDACTED]"} |
JSON object/array supplied as a bounded String |
Parsed for field redaction and formatted as JSON | {"ok":true}; oversized serialized strings are omitted before parsing |
String: plain text, HTML, XML, CSV |
Bounded text with control characters escaped | first line\nsecond line; spaces within text are preserved; no HTML/XML/CSV field parser |
Finite num or bool |
JSON scalar | 42, 3.14, true |
null body |
No body event | No separate body event; completion still prints |
| Empty map/list/string | {}, [] or an empty payload |
{}, [] or a body marker with an empty payload |
FormData |
Fields, file metadata and original counts | One-line JSON; repeated field names remain separate entries |
URL-encoded request supplied as a Map |
Redacted JSON representation of the map | One-line JSON; cURL uses --data-urlencode |
Pre-encoded URL-encoded String |
Plain text | Plain text on one line; use sanitizeText for secrets in encoded strings |
Uint8List |
<bytes: 3> |
Same summary; raw bytes are never dumped |
List<int> response with ResponseType.bytes |
<bytes: 3> |
Same summary |
Ordinary List<int> in a JSON request/response |
JSON array | [1,2,3]; distinct from Uint8List |
Upload Stream<List<int>> |
<runtimeType: not read> |
Same summary; no subscription, buffering or consumption |
Streaming response ResponseBody |
<ResponseBody: not read> |
Same body summary; completion also includes streamReady |
ByteBuffer from custom application code |
<runtimeType: not read> |
Same summary; not automatically treated as a supported raw Dio upload |
| Custom objects, including DTOs | <runtimeType: not serialized> |
Same summary; the logger does not call arbitrary toString() or toJson() |
| Cyclic or oversized values | Markers at configured traversal/output limits | Same markers on one line |
Dio exposes four response modes: json, plain, bytes and stream.
JSON MIME recognition includes application/json, text/json and +json
subtypes such as application/problem+json. A decoded problem-details response
therefore uses the same Map preview as ordinary JSON.
See Dio response modes
and JSON transformation.
Images, PDFs, ZIP archives, protobuf and MessagePack are summarized as bytes
when received with ResponseType.bytes; the logger does not parse these formats.
SSE and NDJSON received as streams remain unread, so logging individual stream
events belongs to the consumer. A complete NDJSON string uses the plain-text
preview. Custom Dio transformers may return other objects; serialize DTOs before
the logger if you want a structured preview.
For comparison, pretty_dio_logger's implementation
formats response maps/lists, prints Uint8List values in chunks, and falls back
to toString() for other types. It combines multipart entries into a map, which
can replace repeated names. Its compact option flattens suitable nested values
inside a boxed response; it is not a single completion-line mode. This package
instead preserves repeated form fields, summarizes bytes/streams, bounds data
before formatting and separates body messages from request/response output.
Body compaction is an independent option.
Multipart and URL-encoded forms #
Body previews show the input observed by the interceptor, before Dio transforms
it into wire bytes. The examples below use requestBody: true and
redaction: const DioLogRedaction(hiddenFields: {'password', 'token'}). Each multiline
example uses compactBody: false; each single-line example uses compactBody: true.
Both layouts work with either request/response mode.
multipart/form-data #
final form = FormData()
..fields.addAll([
const MapEntry('tag', 'one'),
const MapEntry('tag', 'two'),
const MapEntry('token', 'secret'),
])
..files.add(MapEntry(
'upload',
MultipartFile.fromBytes([1, 2, 3], filename: 'photo.png'),
));
await dio.post<dynamic>('/upload', data: form);
Separate body message with compactBody: false:
[HTTP] POST api.example.test/upload [requestBody]
{
"fields": [
{
"tag": "one"
},
{
"tag": "two"
},
{
"token": "[REDACTED]"
}
],
"files": [
{
"upload": {
"filename": "photo.png",
"contentType": "image/png",
"bytes": 3
}
}
],
"fieldCount": 3,
"fileCount": 1
}
Separate body message with compactBody: true:
[HTTP] POST api.example.test/upload [requestBody] {"fields":[{"tag":"one"},{"tag":"two"},{"token":"[REDACTED]"}],"files":[{"upload":{"filename":"photo.png","contentType":"image/png","bytes":3}}],"fieldCount":3,"fileCount":1}
Repeated fields remain separate. Files show their name, content type and declared length, never their contents. The logger does not read or finalize uploads. Counts describe the original form even when preview limits omit some entries.
With showCurl: true, normal mode also includes this command (with request
headers disabled here to focus on the payload):
# Upload files omitted: MultipartFile.filename is not a local path.
curl -i \
-X 'POST' \
--form-string 'tag=one' \
--form-string 'tag=two' \
--form-string 'token=[REDACTED]' \
--url 'https://api.example.test/upload'
convertFormData: false omits multipart payloads from cURL, but keeps the body
metadata preview. requestBody: false removes both the preview and cURL payload.
application/x-www-form-urlencoded #
await dio.post<dynamic>(
'/token',
data: {
'name': 'Jane Doe',
'tag': ['one', 'two'],
'password': 'secret',
},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
Separate body message with compactBody: false:
[HTTP] POST api.example.test/token [requestBody]
{
"name": "Jane Doe",
"tag": [
"one",
"two"
],
"password": "[REDACTED]"
}
Separate body message with compactBody: true:
[HTTP] POST api.example.test/token [requestBody] {"name":"Jane Doe","tag":["one","two"],"password":"[REDACTED]"}
The map is shown as readable JSON, not as percent-encoded wire bytes. The cURL
export uses --data-urlencode so cURL handles spaces and reserved characters, with repeated arguments for list values:
curl -i \
-X 'POST' \
--data-urlencode 'name=Jane Doe' \
--data-urlencode 'tag=one' \
--data-urlencode 'tag=two' \
--data-urlencode 'password=[REDACTED]' \
--url 'https://api.example.test/token'
This is a redacted representation, not a capture of Dio's encoded bytes or
custom list-format/transformer behavior. Pass a map for field-based redaction;
a pre-encoded string such as name=Jane+Doe&password=secret is plain text to the
logger and requires a suitable sanitizeText callback to hide its secrets.
Timing and metadata #
Elapsed time is measured with a Stopwatch for each observed attempt. It covers
the observed interceptor pipeline, not individual DNS, TLS, TTFB or server timings.
Stream responses are labeled streamReady: the transfer and file write can
complete or fail later. Stream consumption and
download completion need instrumentation at their owning layer.
HTTP version and declared Content-Length are recorded when available. They
depend on the adapter/response; Content-Length is not measured wire traffic.
Normal mode can include redirect and response metadata sections through
showRedirects and showResponseMetadata. Redirect and final-URI metadata depend
on the adapter. All Dio error types, including
transformTimeout, are preserved. Retry attempts remain separate; an earlier
interceptor resolving/rejecting without forwarding can bypass this interceptor.
Place the logger after middleware whose final request changes you want to see.
Tests #
dart format --set-exit-if-changed -o none .
dart analyze --fatal-infos
dart test
dart test --platform chrome
dart test --coverage-path=coverage/lcov.info --coverage-package='^carapacik_dio_logger$' --branch-coverage
The coverage report includes this package's library code, excluding dependencies.