Periscope reads config/periscope.ts during application boot. Wrap the object with defineConfig
for type checking, default resolution, and strict validation:
import { defineConfig } from '@rikology/adonisjs-periscope/periscope_config'
export default defineConfig({
applicationName: 'billing-api',
})Unknown keys and invalid values fail boot with a PeriscopeConfigError that reports every detected
problem. Nested objects merge over the defaults. Arrays replace their defaults rather than extending
them.
Runtime switches
| Option | Type | Default | Behavior |
|---|---|---|---|
enabled |
boolean |
true |
Master switch before environment and runtime overrides. |
applicationName |
string |
'default' |
Stable label stamped onto every entry. Required to distinguish applications sharing one store; maximum 191 characters. |
enabledIn |
string[] |
['development', 'test'] |
Allowed NODE_ENV values. An empty array is rejected; use enabled: false instead. |
PERISCOPE_ENABLED has final precedence. 1 and true force recording on; 0 and false force it
off. Matching ignores case and surrounding whitespace. Any other value is ignored and normal config
resolution applies.
When recording is disabled, the provider installs no watcher, logger, process, model, or dashboard hooks.
Storage
storage: {
driver: 'sqlite-local',
maxEntries: 10_000,
retention: {
hours: 48,
keepExceptions: true,
perType: {
query: { hours: 12 },
mail: { hours: 168 },
},
},
}| Option | Type | Default | Behavior |
|---|---|---|---|
storage.driver |
'sqlite-local' | 'database' | 'memory' | 'custom' |
'sqlite-local' |
Selects the persistence implementation. |
storage.connection |
string |
Lucid default | Connection name used only by the database driver. |
storage.factory |
PeriscopeStoreFactory |
none | Required with custom; rejected for shipped drivers. |
storage.maxEntries |
positive integer | 10_000 |
Hard entry ceiling. The oldest entries are trimmed after writes. |
storage.retention |
object | disabled | Enables age-based pruning after boot and every 15 minutes. |
storage.retention.hours |
positive integer | required | Default retention window for every entry type. |
storage.retention.keepExceptions |
boolean |
false |
Keeps exception entries regardless of age. |
storage.retention.perType |
entry-type map | {} |
Overrides the retention window for selected entry types. |
Driver selection:
sqlite-localstores a dedicated SQLite database undertmp/and requires no Lucid setup.databaseuses the application’s Lucid connection and requires the shipped migration.memoryis process-local and disappears on exit. Maintenance commands reject it because Ace runs in a separate process.customcallsstorage.factory({ app, config }). See Authoring adapters.
For shared databases, assign a unique applicationName to every application. See
Operations for deployment and
pruning guidance.
Recording
| Option | Type | Default | Behavior |
|---|---|---|---|
recording.caps |
entry-type map | { default: 100, query: 200 } |
Maximum accepted entries of each type within one batch. 0 disables storage for that type without subscribing or unsubscribing its watcher. |
recording.sampleRate |
number from 0 to 1 |
1 |
Retention decision made once when a batch opens. |
recording.keepAlways |
(batch) => boolean |
() => false |
Retains a sampled-out batch after inspecting its completed entry metadata. |
recording.ambientRotationMs |
positive integer | 10_000 |
Flush interval for work outside request, command, queue, schedule, or test scopes. |
recording.pausedFlagTtlMs |
positive integer | 5_000 |
Cache window for the shared pause flag. Lower values make pause/resume propagate faster at the cost of more reads. |
recording.lateEntryGraceMs |
non-negative integer | 2_000 |
Window in which a finished batch accepts late asynchronous entries. 0 drops late entries. |
Caps limit a noisy batch; storage.maxEntries limits the whole store. Sampling drops or keeps the
entire batch, preserving request correlation:
recording: {
sampleRate: 0.1,
caps: { default: 50, query: 100, mail: 5 },
keepAlways: (batch) =>
batch.hasEntryOfType('exception') ||
batch.hasTag('important') ||
batch.hasEntryWhere((entry) => Number(entry.content.durationMs) > 1_000),
}Monitored tags also retain sampled-out batches. Use exact tags; tag matching is not substring-based.
Redaction
| Option | Type | Default | Behavior |
|---|---|---|---|
redact.keys |
string[] |
DEFAULT_REDACT_KEYS |
Scrubs matching keys at any depth. Matching ignores case, underscores, hyphens, and spaces. |
redact.headers |
string[] |
DEFAULT_REDACT_HEADERS |
Scrubs matching HTTP header names. |
redact.valuePatterns |
RegExp[] | false |
DEFAULT_REDACT_VALUE_PATTERNS |
Scrubs secret-shaped substrings in captured strings. false disables value scanning only. |
redact.replacement |
string |
'[REDACTED]' |
Replacement value written to storage. |
Redaction arrays replace the shipped defaults. Spread the exported constants to extend them:
import {
DEFAULT_REDACT_HEADERS,
DEFAULT_REDACT_KEYS,
DEFAULT_REDACT_VALUE_PATTERNS,
REDACT_EMAIL_PATTERN,
defineConfig,
} from '@rikology/adonisjs-periscope/periscope_config'
export default defineConfig({
redact: {
keys: [...DEFAULT_REDACT_KEYS, 'internal_reference'],
headers: [...DEFAULT_REDACT_HEADERS, 'x-internal-token'],
valuePatterns: [...DEFAULT_REDACT_VALUE_PATTERNS, REDACT_EMAIL_PATTERN],
},
})Filter hooks run before redaction and therefore see raw values. Tag hooks run after redaction. Exports do not run a second redaction pass; review exported files as sensitive artifacts.
Hooks
hooks: {
filter: [(entry) => entry.type !== 'log'],
tag: [
(entry) =>
entry.type === 'request' ? ['application-traffic'] : [],
],
}| Option | Type | Default | Behavior |
|---|---|---|---|
hooks.filter |
FilterHook[] |
[] |
Returning false drops an entry before buffering and redaction. |
hooks.tag |
TagHook[] |
[] |
Returned tags are appended after redaction and de-duplicated. |
Hook failures are safeguarded so diagnostic customization cannot break application work.
Watchers
Every watcher accepts enabled. A disabled watcher subscribes to nothing. Core framework watchers
are enabled by default; infrastructure integrations and capture of application-owned payloads are
opt-in.
| Watcher | Default | Additional options and defaults |
|---|---|---|
request |
on | slowMs: 1_000, captureResponse: true, captureInertia: true, responseSizeLimitKb: 64, captureSession: true, captureStatic: false, ignorePaths: [] |
query |
on | slowMs: 100, hideBindings: false |
exception |
on | captureCodeFrame: 'dev', captureProcessErrors: true; code-frame modes are dev, always, and never |
log |
on | level: 'warn'; accepted levels are trace, debug, info, warn, error, and fatal |
event |
on | ignore: [] |
command |
on | ignore: [], captureOutput: true |
mail |
on | no additional options |
cache |
on | captureValues: false |
model |
on | captureDirty: false |
gate |
on | ignoreAbilities: [] |
dump |
on | no additional options |
view |
on | captureDataKeys: true |
http_client |
on | slowMs: 1_000 |
health_check |
on | no additional options |
vine |
on | no additional options |
i18n |
on | no additional options |
transmit |
off | capturePayload: false |
job_schedule |
off | adapters: [], schedulers: [], capturePayload: false |
redis |
off | captureArguments: false |
session |
off | captureValues: false |
limiter |
off | no additional options |
lock |
off | contentionMs: 50 |
drive |
off | no additional options |
ally |
off | no additional options |
notification |
off | adapters: [], capturePayload: false |
socket |
off | adapters: [], capturePayload: false |
custom |
empty | Array of PeriscopeWatcherFactory functions, registered after shipped watchers. |
Globs in request.ignorePaths and event.ignore use * for any run of characters. The log watcher
cannot recover messages already filtered by Pino, so its effective floor is the stricter of the
application logger level and watchers.log.level.
See Watchers and troubleshooting for the signal catalogue and Authoring adapters for queue, scheduler, notification, socket, custom watcher, store, and fanout contracts.
Dashboard
| Option | Type | Default | Behavior |
|---|---|---|---|
dashboard.path |
absolute path | '/periscope' |
Mount point for the SPA, JSON API, SSE stream, and assets. Trailing slashes are removed. |
dashboard.authorize |
(ctx) => boolean | Promise<boolean> |
allow non-production | Evaluated for every dashboard request. Returning false produces 403. |
dashboard.nPlusOneThreshold |
positive integer | 5 |
Equal query shapes in one batch required for an N+1 candidate. |
dashboard.sseMaxClients |
positive integer | 5 |
Maximum concurrent live-stream clients per process. |
dashboard.fanout |
FlushFanoutFactory |
in-process | Optional cross-process pub/sub bridge for live updates. |
Changing dashboard.path does not weaken the production gate. For deliberate production exposure,
replace dashboard.authorize with an application-specific policy and follow the
dashboard security checklist.
The default fanout reaches only clients connected to the worker that recorded an entry. Durable storage still makes entries visible to polling clients on other workers. See Multi-process live updates.
Validation and merge rules
- Unknown keys are errors, including misspelled watcher and entry-type names.
- Nested objects merge over defaults.
- Arrays replace defaults and are copied during resolution.
recording.capsprecedence is an explicit entry type, then the supplieddefault, then the built-in type default.storage.retention.perTypeaccepts only known entry types and requires a positivehoursvalue.storage.factoryis required only fordriver: 'custom'.dashboard.pathmust start with/.- Integer limits reject negative, fractional, infinite, and unsafe values.