RUNNING TESTS
Logging
Karate v2 has a single bucket for everything log-related: configure logging. It controls what gets captured into reports, what hits the console, how HTTP bodies are formatted, and which fields get redacted.
TL;DR
function fn() {
karate.configure('logging', {
report: 'debug', // what gets captured into reports
console: 'info', // what hits stdout via SLF4J/Logback
pretty: true, // pretty-print HTTP JSON bodies
mask: { // HTTP-only redaction
headers: ['Authorization', 'Cookie'],
jsonPaths: ['$.password', '$..token'],
patterns: [{ regex: '\\bBearer [A-Za-z0-9._-]+\\b', replacement: 'Bearer ***' }]
}
});
return { baseUrl: 'https://api.example.com' };
}
The same shape works inside a feature file:
Background:
* configure logging = { report: 'debug', console: 'info', pretty: true }
Both forms — karate.configure('logging', {...}) in karate-config.js and * configure logging = {...} in a feature — apply for the whole scenario, including HTTP mask and pretty. Mid-test mutations are auto-snapshot/restored at scenario end, so a partial change in one scenario does not leak into the next.
Two thresholds: report vs console
Karate keeps two independent levels.
| Knob | What it controls | CLI flag | Default |
|---|---|---|---|
logging.report | What gets captured into HTML / JSONL / Cucumber JSON / JUnit | --log-report | debug |
logging.console | What hits stdout via SLF4J / Logback | --log-console | info |
The HTTP logger always writes the full request/response (headers + bodies) to the report buffer at INFO. The console emission tiers automatically by SLF4J level: INFO = one-liner, DEBUG = +headers, TRACE = +body. Splitting the two knobs lets you, for example, capture full traces in reports for post-hoc debugging while keeping a parallel run's console quiet.
You do not need to set console: 'trace' to see request and response bodies. The HTML report already captures the full request and response — headers and bodies — at default settings. Open target/karate-reports/karate-summary.html and click into a step.
The console is auto-tiered separately so a real test run doesn't dump bodies onto your terminal. If you specifically want bodies streaming to stdout (for live local debugging), set console: 'trace' — but for verifying what was actually sent and received, the HTML report is the right tool.
Recipe table
Pick the row that matches what you want — most users want the first one, which is the default.
| What you want | report | console | Effect |
|---|---|---|---|
| Bodies in HTML report, quiet console | debug | info | Default. One-liner per request on stdout; full headers + body in HTML / JSONL / JUnit / Cucumber. |
| Headers on console, bodies in report | debug | debug | Useful when you want to see URLs and headers fly by without dumping bodies on screen. |
| Bodies on console too | debug | trace | Streams full request + body to stdout. Noisy — use locally for debugging, not CI. |
| Quiet everything | warn | warn | HTTP request lines drop out of HTML too. Last resort for sensitive runs — see also @report=false. |
# Quiet console, rich reports — typical CI setup
* configure logging = { report: 'debug', console: 'warn' }
# Bodies on console too — local debugging, noisy
* configure logging = { console: 'trace' }
v1 wired everything to a single Logback level and emitted full HTTP bodies on the console at DEBUG. v2 splits report capture from console emission, and the console is auto-tiered: INFO = one-liner, DEBUG = +headers, TRACE = +body.
If your v1 muscle memory was "set DEBUG to see bodies in the terminal", in v2 just open the HTML report — bodies are there at default settings. Set console: 'trace' only if you really want them on stdout.
Mid-test level flips with auto-restore
* configure logging = { ... } mid-flow takes effect immediately. At scenario end the levels are automatically snapshotted and restored, so the next scenario starts at whatever karate-config.js set. No manual cleanup needed.
Scenario: silence a noisy reusable
* configure logging = { report: 'error' }
* call read('classpath:noisy-warmup.feature')
# report level is back to default at scenario end — automatically
Pretty body formatting
logging.pretty applies to HTTP request/response JSON bodies in both console and report.
* configure logging = { pretty: true } # default — multi-line, 2-space indent
* configure logging = { pretty: false } # single line
Non-JSON bodies (XML, plain text, binary) pass through unchanged. The pretty pass runs after masking, so masked values stay masked.
Masking sensitive data
logging.mask is a declarative redactor that runs against HTTP request/response logs. It does not scan print or karate.log output — those are user-controlled channels. If a step might leak via print, raise logging.report to 'warn' to drop INFO captures, or tag the scenario @report=false.
Headers
Header names are matched case-insensitively (RFC 7230). Matched headers' values are replaced with replacement (default '***').
* configure logging = {
mask: { headers: ['Authorization', 'Cookie', 'X-Api-Key'] }
}
* header Authorization = 'Bearer abc.def.ghi'
# Logged as: Authorization: ***
JSON paths
Two simple forms are supported:
$.a.b.c— descend by key$..foo— recursive search for anyfookey at any depth
Matched values become replacement.
* configure logging = {
mask: { jsonPaths: ['$.password', '$..token', '$.user.ssn'] }
}
* request { username: 'alice', password: 'hunter2', meta: { token: 'shhh' } }
# Logged body:
# {
# "username": "alice",
# "password": "***",
# "meta": { "token": "***" }
# }
Regex patterns
Each rule is { regex, replacement }. Patterns run after header / JSON-path masking, so they catch anything those passes didn't.
* configure logging = {
mask: {
patterns: [
{ regex: 'Bearer [A-Za-z0-9._-]+', replacement: 'Bearer ***' },
{ regex: '\\b\\d{16}\\b', replacement: '****-****-****-****' }
]
}
}
URL-scoped masking
enableForUri(uri) is an optional JS predicate. When it returns falsy for a request, no masking applies — useful for excluding /health or other low-value endpoints from redaction so debugging stays easy.
* configure logging = {
mask: {
headers: ['Authorization'],
enableForUri: function(uri){ return uri.indexOf('/health') < 0 }
}
}
Customizing the replacement
* configure logging = {
mask: {
headers: ['Authorization'],
replacement: '<redacted>'
}
}
# Logged as: Authorization: <redacted>
Verifying the mask is active
When the runtime logger (karate.runtime) is at DEBUG, Karate emits a one-line confirmation per scenario showing which mask compiled and how many rules of each type are in effect:
karate.runtime - http log mask active: 3 headers, 2 jsonPaths, 1 pattern
If you don't see this line and you expected masking to be on, check:
maskwas set underconfigure logging = { mask: {...} }, not at the top level.- At least one of
headers,jsonPaths, orpatternshas entries — an emptymaskwarns and disables itself. - Pattern regexes are valid — invalid ones log
configure logging.mask.patterns[N] invalid regex ...and are skipped.
Hide a scenario from reports — @report=false
Tag a scenario @report=false to keep it in the run (it still counts toward suite totals) but suppress its step detail from HTML / Cucumber JSON / JUnit XML / JSONL outputs.
@report=false
Scenario: warmup with sensitive credentials
* call read('classpath:auth/login.feature')
Behavior:
- The scenario runs normally and contributes to pass / fail / skip counts.
- HTML / Cucumber / JUnit / JSONL show the row + status only — no step text, no captured logs, no embedded screenshots.
- Failures surface a redacted message:
output suppressed by @report=false (full detail in runtime logs). - Full failure detail still hits SLF4J / Logback, so you can debug locally with
--log-console debug. - Suppression propagates: any feature called from a
@report=falsescenario is also hidden.
Use this for any run that may include secrets in HTTP bodies or error messages and where the artifacts get uploaded somewhere (CI archives, S3, Slack, etc.).
Deep merge
configure logging deep-merges with the parent. A partial update keeps everything else.
# Background sets the global mask + pretty
Background:
* configure logging = { mask: { headers: ['Authorization'] }, pretty: false }
# A later scenario flips just the threshold — mask + pretty survive
Scenario:
* configure logging = { report: 'error' }
SLF4J categories
For finer console control beyond a single threshold, Karate emits under named SLF4J categories. Tune them in logback.xml or via your dependency-injected logger config.
| Category | What it covers |
|---|---|
karate.runtime | Feature/scenario lifecycle, step execution, suite orchestration |
karate.http | HTTP client — request/response bodies, headers, retries |
karate.mock | Mock server — incoming requests, matched scenarios, responses |
karate.server | Embedded HTTP server — request/response logs |
karate.scenario | User output — print, karate.log(), scenario-scoped messages |
karate.console | CLI console output — summary, colors, progress |
Example: DEBUG only for HTTP traffic, INFO everywhere else:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder><pattern>%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern></encoder>
</appender>
<logger name="karate.http" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
logging.console is a convenience that sets the karate parent logger's level. For per-category control, edit logback.xml directly.
Putting it together — environment-driven logging
function fn() {
var env = karate.env || 'dev';
var config = { baseUrl: 'https://api.example.com' };
if (env === 'dev') {
karate.configure('logging', {
report: 'debug', console: 'debug',
pretty: true
});
} else if (env === 'ci') {
karate.configure('logging', {
report: 'debug', // capture rich detail in artifacts
console: 'warn', // keep CI logs quiet
pretty: true,
mask: {
headers: ['Authorization', 'Cookie', 'X-Api-Key'],
jsonPaths: ['$.password', '$..token', '$..secret'],
patterns: [
{ regex: 'Bearer [A-Za-z0-9._-]+', replacement: 'Bearer ***' }
]
}
});
}
return config;
}
CLI
# Set both thresholds at run time
karate run --log-report debug --log-console warn features/
# Or via karate-pom.json (`logging` block)
{
"paths": ["src/test/features"],
"logging": {
"report": "debug",
"console": "info"
}
}
Mask configuration lives in feature files / karate-config.js, not the CLI — it's declarative and belongs with the test code that knows what's sensitive.
See also
- Test Reports — formats, output directories, CI publishing
- Debugging — using captured logs to troubleshoot
- Tags —
@report=falseand other built-in tags - Configuration — the
configurekeyword andkarate-config.js