Api Spec
On this page
- Status (v1 as implemented)
- 1. Architecture
- 2. Collector SPI
- 3. Event inventory (BoxLang core 1.19, read from source)
- 3.1 Request lifecycle (announced globally and per app with Key.* )
- 3.2 Execution
- 3.3 Data
- 3.4 I/O and misc
- 3.5 Global, not per request (Runtime/Admin panels)
- 3.6 Removed from the current code
- 3.7 Core gaps to request
- 4. Settings ( boxlang.json > modules > bxLens > settings)
- 5. BIF / component API
- 6. Injection
- 7. Security
- 8. UI
- 8b. Module extension API (modules contribute collectors and panels)
- 9. Delivery plan
- 10. Open items
BxLens: API and Shape Spec (DRAFT for approval)
Request-level debug bar for BoxLang web apps (MiniServer, servlet, CommandBox). In-memory only. HTML responses only. Observability across requests is out of scope (that is BX Insights). BxLens is a product of Ortus Solutions, see Licensing.
Status (v1 as implemented)
This page is the original design draft and is kept for history. Where it disagrees with the shipped module, the user docs win. The settings below (enabled, access.allowedIPs, access.allowPrivateNetworks) are replaced by bar.* and console.*, and the console, the bar designer, request cost and licensing came later: see Console, Configuration, BIF Reference, Extending Lens and Core Event Inventory.
Implemented in v1
- Collectors: templates, functions (opt in), queries, HTTP, exceptions, messages, timers, transactions, logs (opt in), scopes, JVM (Runtime), cache, modules, plus request lifecycle data.
- Panels: Issues, Timeline, Queries, Templates, HTTP, Exceptions, Messages, Timers, Cache, Modules, Request, Scopes, Runtime and History.
- BIFs:
lensMessage,lensDump,lensStart,lensStop,lensMeasure,lensAddMeasure,lensException,lensEnable,lensDisable,lensIsEnabled,lensRender,lensPanel. - Tier 1 extension:
onLensRegister,onLensRequestStart,onLensCollect,onLensRequestFinish, with the table, kv, tree, spans, messages, json and text renderers. - Editor links with
editor.remoteBaseandeditor.localBasepath mapping. - In-memory History ring buffer (default 50) with JSON, SSE and other non-HTML requests.
Changed from this draft
- The settings are exactly the keys in
ModuleConfig.bx. There is noinjectPosition, noX-BxLenscookie or header override, nocollectors.request,timelineorsession, and slow thresholds live only underthresholds. Added:limits,collectors.transactions,collectors.logs.max,collectors.queries.captureCaller,collectors.scopes.variablesanddev.reloadAssets.collectors.cacheis on by default. - There is no
/~bxlens/asset endpoint. Lens inlines CSS, JavaScript and data into the HTML response, about 110 KB each. - Detach opens a floating window inside the page with a Dock button, not a separate browser window.
- History shows summaries only. Loading the detail of an earlier request is not available.
- Uncaught exceptions and aborts skip
onRequestEnd, and the web context does not announceonRequestFlushBuffer, so no bar is injected on core's error page. The request is still recorded in History. - The Cache panel computes per-request numbers from the difference of cache statistics, because core announces no cache read events.
- Redaction matches any key that contains a redact key, ignoring case.
- The JVM panel is named Runtime.
Planned, not implemented
lensDumpHeap,lensThreadDump,allowHeapDump,collectors.jvm.threadDump.- The
bx:lenscomponent. - Tier 2 (Java collectors) and Tier 3 (custom UI,
ui.allowCustomPanels). - Compare two requests.
- BIF call collector, components panel and SOAP events, which need core changes (section 3.7).
1. Architecture
| Piece | Responsibility |
|---|---|
LensService (global service) | Owns config, collector registry, RequestStore, GlobalStats. |
LensRequest | Typed per-request data. Stored as a context attachment on the request context. |
ILensCollector | SPI. One class per panel. Java only. Registered as interceptors by the service. |
RequestStore | Bounded ring buffer of finished LensRequest snapshots (default 50). No disk, no DB. |
LensInjector | Interceptor that renders the bar into HTML responses at end of request. |
| Asset endpoint | Planned in this draft, not implemented. v1 inlines the assets in the page instead. |
Request flow: onRequestStart creates LensRequest (if allowed) then collectors fill it then onRequestEnd / onError / onAbort finalize, store, and inject.
2. Collector SPI
public interface ILensCollector {
String id(); // "queries"
String label(); // "Queries"
String icon(); // icon key used by the UI
int order(); // tab order
void configure( LensConfig cfg );// called once, cfg is immutable and cached
void onRequestStart( LensRequest req ); // optional setup, default no-op
void onRequestFinish( LensRequest req ); // optional snapshot work, default no-op
Object payload( LensRequest req ); // JSON-safe: records, maps, lists, numbers, strings
Badge badge( LensRequest req ); // count + severity for the tab (NONE, WARN, CRIT)
}
Rules:
- Collectors never throw into a request. Failures are logged to the
bxLenslogger and the collector is skipped for that request. - Per-request state lives in
LensRequest.slot( id ), never in collector fields (collectors are shared across threads). - Hot paths use typed records and preallocated bounded buffers. No
Map<String,Object>per event. - Time uses
System.nanoTime()offsets from request start. - Third parties can add collectors via
LensService.register( ILensCollector )from their own module.
3. Event inventory (BoxLang core 1.19, read from source)
Payload keys below are what core actually sends. The current collectors read keys that do not exist (for example params/recordCount on postQueryExecute, bodyLength on onHTTPRequest), so they must be rewritten against this table.
3.1 Request lifecycle (announced globally and per app with Key.*)
| Event | Payload | Use |
|---|---|---|
onRequestStart | context, args, application, listener | create LensRequest, access check |
onRequest | same | template/class entry |
onRequestEnd | same | finalize, store, inject. Skipped on unhandled exceptions and aborts (WebRequestExecutor) |
onError | same (args holds the exception) | exception capture, finalize and inject on error pages |
onAbort | same | finalize and inject on abort |
onMissingTemplate | same | 404 issue |
onSessionStart, onSessionEnd | same | session panel |
onRequestFlushBuffer | context, output (mutable) | alternative injection point, fires on every flush so only the last flush may be modified |
onApplicationStart/End/Restart/Defined, beforeApplicationListenerLoad, afterApplicationListenerLoad | app data | App panel |
onWebExecutorRequest (web-support) | context, appListener, requestString, exchange, updatedRequest | route rewrites, request identity |
3.2 Execution
| Event | Payload | Use |
|---|---|---|
preTemplateInvoke, postTemplateInvoke | context, template, templatePath | templates/includes tree. No executionTime, we time it ourselves |
preFunctionInvoke, postFunctionInvoke | context, arguments, function, name (+ result on post) | functions panel (opt in, hot path) |
onFunctionException | same + exception | exceptions |
onBIFInvocation | context, arguments, bif, name (+ result on 2nd call) | BIF panel. Core bug: the post-call re-announces onBIFInvocation instead of postBIFInvocation, so the post event never fires. Workaround: detect the call carrying result. File a Jira |
onPreSourceInvoke, onPostSourceInvoke | script/source execution | eval/script timing |
afterBoxClassCreation, afterBoxClassInit, afterDynamicObjectCreation, onCreateObjectRequest | object created | Objects panel (opt in) |
onComponentInstance, onBIFInstance | descriptor | startup only |
onComponentInvocation | declared in BoxEvent, never announced by core | cannot be used today. Needs a core change for a Components panel |
3.3 Data
| Event | Payload | Use |
|---|---|---|
onQueryBuild | query build data | builder usage |
preQueryExecute | sql, bindings, pendingQuery, context | start time, SQL text |
postQueryExecute | sql, bindings, executionTime, data, result (meta), pendingQuery, executedQuery, context | queries panel; row count from data, datasource from pendingQuery |
queryAddRow | query row | ignore |
onTransactionBegin/Acquire/Commit/Rollback/SetSavepoint/Release/End | connection, transaction, context | transactions panel, shown as spans on the waterfall |
onDatasourceConfigLoad, onDatasourceStartup, onDatasourceInitialized, service start/stop | datasource | JVM/Runtime panel |
afterCacheElementInsert/Updated/Removed, beforeCacheElementRemoved, afterCacheClearAll | cache element | cache writes only. No read/hit/miss event |
onCacheComponentAction | cache component action | output cache usage |
3.4 I/O and misc
| Event | Payload | Use |
|---|---|---|
onHTTPRequest | result, httpClient, httpRequest | HTTP panel start |
onHTTPRawResponse, onHTTPResponse | result, response, httpClient, httpRequest | status, size, time |
onHTTPError | error data | issue |
onFileComponentAction | file action | Files panel (opt in) |
logMessage | text, log, type | Logs panel |
onBXDump / onMissingDumpOutput | dump data | route bx:dump output into Messages instead of the page body |
beforeObjectMarshallSerialize etc. | serialization | ignore |
3.5 Global, not per request (Runtime/Admin panels)
onSchedulerStartup/Shutdown/Restart, schedulerBeforeAnyTask, schedulerAfterAnyTask, schedulerOnAnyTaskSuccess, schedulerOnAnyTaskError, onSchedulerRegistration/Removal, onAllSchedulersStarted, onWatcher*, module events (preModuleLoad, postModuleLoad, ...), onRuntimeStart, onRuntimeShutdown, onConfigurationLoad. These feed a rolling global activity view (scheduled tasks, executors, watchers) kept in LensService, not in LensRequest.
3.6 Removed from the current code
onException, onSOAPRequest, onSOAPResponse do not exist in core.
3.7 Core gaps to request
postBIFInvocationis never announced (bug).onComponentInvocationis never announced.- No cache read/hit/miss event.
- No SOAP events.
- Template events lack execution time (cheap to add, optional).
4. Settings (boxlang.json > modules > bxLens > settings)
{
"enabled": false,
"access": {
"allowedIPs": [ "127.0.0.1", "::1" ],
"allowPrivateNetworks": true,
"allowedHosts": [],
"requireHeader": ""
},
"inject": true,
"injectPosition": "bodyEnd",
"contentTypes": [ "text/html" ],
"excludePaths": [ "/~bxlens/*", "/favicon.ico" ],
"history": { "trackNonHtml": true, "header": "X-BxLens-Id", "maxRequests": 50 },
"ui": { "theme": "auto", "startOpen": false, "autoOpenOnException": true, "defaultTab": "timeline", "height": 360, "allowDetach": true, "hotkey": "Ctrl+`" },
"thresholds": { "slowRequestMs": 500, "slowQueryMs": 25, "slowTemplateMs": 100, "nPlusOneMin": 3 },
"editor": { "linkPattern": "vscode://file/{path}:{line}", "remoteBase": "", "localBase": "" },
"redact": { "keys": [ "password", "pwd", "token", "secret", "apikey", "authorization", "cookie" ], "mask": "[redacted]" },
"collectors": {
"request": { "enabled": true },
"timeline": { "enabled": true },
"templates": { "enabled": true, "max": 300 },
"functions": { "enabled": false, "max": 1000, "minMs": 0 },
"queries": { "enabled": true, "max": 200, "slowMs": 25, "detectDuplicates": true, "detectNPlusOne": true, "includeParams": true },
"http": { "enabled": true, "max": 100 },
"exceptions": { "enabled": true, "max": 50 },
"messages": { "enabled": true, "max": 200 },
"timers": { "enabled": true, "max": 200 },
"scopes": { "enabled": true, "form": true, "url": true, "cookie": false, "session": false, "request": false, "application": false, "maxDepth": 4, "maxBytes": 65536 },
"jvm": { "enabled": true, "threadDump": false },
"cache": { "enabled": false },
"logs": { "enabled": false, "minLevel": "INFO" },
"session": { "enabled": false },
"modules": { "enabled": true }
}
}
Config is parsed once at activation into an immutable LensConfig. Unknown keys are logged as warnings.
Per request overrides: lensEnable() / lensDisable(), and a cookie/header X-BxLens: on|off honored only when access allows the caller.
5. BIF / component API
| Name | Signature | Notes |
|---|---|---|
lensMessage | ( message, label="info" ) | label: info, warn, error, debug or any string. Structs/arrays are serialized. |
lensStart | ( label ) : id | Starts a timer. |
lensStop | ( labelOrId ) | Ends a timer. Unmatched labels are ignored. |
lensMeasure | ( label, callable ) : any | Times a closure and returns its result. |
lensAddMeasure | ( label, startMs, endMs ) | Manual span. |
lensException | ( exception ) | Records a caught exception. |
lensEnable / lensDisable | () | Per request switch (still subject to access). |
lensRender | () : string | Manual render when inject=false. |
lensIsEnabled | () : boolean | New. |
lensDump | ( value, label="" ) | New. Pushes a value to the Messages panel with a collapsible tree. |
bx:lens | action="message|start|stop|dump" | New. Component form for templates. |
Removed from the default surface: lensDumpHeap, lensThreadDump. They stay behind collectors.jvm.threadDump and a separate opt-in allowHeapDump setting, and never write outside the temp dir.
6. Injection
- Hook: interceptor on
onRequestEndwith fallbacks ononErrorandonAbort. Pattern follows web-supportHtmlBody. - Inject only when: lens enabled, caller allowed, response
Content-Typestarts with an entry incontentTypes, status is not a redirect, response not already committed, request not matched byexcludePaths. - Place before the last
</body>(case-insensitive search from the end, no full DOM parse). If absent, append. - Non-HTML (JSON, SSE, files, redirects): no bar. When
history.trackNonHtmlis on (default), the request is still collected and stored, and the id is returned in theX-BxLens-Idheader so it appears under History. History is a ring buffer: when full, the oldest request is recycled. - The bar markup is a small container plus a JSON payload (
<script type="application/json">,</escaped). In this draft, behavior and styles load from/~bxlens/. v1 inlines them instead.
7. Security
- Default
enabled=false. When on, default access is loopback and private networks only. - Redaction is server side, before serialization, on scopes, headers, params and messages.
- Payload size caps per collector (
maxBytes,max). - No eval in the UI: use the Alpine CSP build (
@alpinejs/csp), assets served from the module, no inline handlers. - Heap dump and thread dump are opt-in.
- JSON is embedded with
<,>,&, U+2028, U+2029 escaped as unicode escapes, not HTML entities.
8. UI
Decisions (from design review):
- Collapsed health strip by default: status, time, memory, query and template counts. Strip border and an issues chip turn amber or red on slow request, N+1, caught exception, or 4xx/5xx.
- Auto-opens to the relevant tab when an exception is caught or thrown (
ui.autoOpenOnException). - Hero view is a unified waterfall: templates, functions, queries and HTTP calls on one time axis with nesting. Must-haves: hover details and click-to-expand drawer, zoom and pan the time axis, filter by type plus text search, open in editor from any row.
- Issues tab lists everything suspicious (exceptions, N+1, slow queries, slow templates) ranked, each linking to its row.
- Docked at the bottom, resizable by dragging the top edge, and detachable into a floating window inside the page. Open tab, height and collapsed state persist per browser.
- Hotkeys: Ctrl+` toggles, 1-9 switch tabs, / focuses search.
- Copy actions: SQL with params, file:line, request JSON, cURL.
- History shows HTML, JSON, SSE and other requests, with type, status, time and issue count. Compare-two-requests is post-v1.
- Compact devtools density. Brand gradient used sparingly (active tab, healthy status), not as a wash.
Alpine.js (kept per decision). Mockup: https://claude.ai/artifact/AQsvxZRZrKqWraet41gKno
Palette from ortus-artwork/boxlang: gradient #00DBFF to #00FF75, dark ground derived from #303446. Light and dark themes. Panels: Timeline (waterfall), Queries, Templates (include/call tree), HTTP, Exceptions, Messages, Timers, Request, Scopes, JVM, History.
8b. Module extension API (modules contribute collectors and panels)
Problem: every BoxLang module has its own class loader, so another module cannot implement Java interfaces that live inside bx-lens. The contract therefore has to be data-first, with Java as an optional second tier.
Tier 1: data-only contributions (v1). No shared classes. Works from BoxLang or Java modules.
- bx-lens registers its own interception points in
configure()ofModuleConfig.bx:
| Point | When | Payload |
|---|---|---|
onLensRegister | at bx-lens activation and again on postModuleLoad | registry with registry.panel( id, label, icon, order, renderer ) |
onLensRequestStart | per request, after LensRequest exists | context, requestId |
onLensCollect | per request, before render | context, requestId, panel( id ) handle to add data |
onLensRequestFinish | per request, after store | requestId, summary |
- A module declares panels once and fills them per request:
// in another module's ModuleConfig.bx
function configure() {
interceptors = [ { class: "#moduleMapping#.interceptors.MyLens", name: "MyLens@mymodule" } ];
}
// MyLens.bx
@InterceptionPoint
function onLensRegister( event ) {
event.registry.panel( id: "orm", label: "ORM", icon: "database", order: 40, renderer: "table" );
}
@InterceptionPoint
function onLensCollect( event ) {
event.panel( "orm" )
.columns( [ "Entity", "Action", "ms" ] )
.rows( ormStats.rows )
.badge( ormStats.count, ormStats.slow ? "warn" : "none" );
}
-
App code can use the same thing without a module through BIFs:
lensPanel( id, label ).table( ... ),.kv( ... ),.tree( ... ),.spans( ... ),.messages( ... ),.json( ... ). -
Renderers are built in, so module panels need no JavaScript:
table,kv,tree,spans(feeds the waterfall with a type, label, start, duration),messages,json,text. Spans from any panel can appear in the Timeline with their own color and filter chip. -
Contributions can add Issues (
panel.issue( severity, title, detail, file, line )) which show on the Issues tab and drive the strip color. -
Safety: payloads go through the same redaction, size caps and JSON escaping as built-in collectors. Text is always escaped by the UI. No module-supplied HTML or script in Tier 1.
Tier 2: Java collectors (v1.x). For hot paths where a module wants typed, fast collection. Options, to decide: publish a tiny bx-lens-api jar (interfaces only) that modules include in their own libs/, and bx-lens talks to it reflectively or via a ServiceLoader keyed on interface name; or keep Java modules on Tier 1 and let them call a static facade. This needs a spike on module class loader parents before we commit.
Tier 3: custom UI (later). A module may ship a small ES module served from /~bxlens/ext/{module}/ and declared with renderer: "custom". Disabled by default behind ui.allowCustomPanels, since it runs script in the page.
Candidate first-party contributors: bx-orm (entity loads and flushes), bx-redis (commands), bx-mail (sent mail), bx-ai (calls and tokens), bx-jdbc drivers (pool stats), Quick/qb, ColdBox/cbwire (event, handler, layout, view).
9. Delivery plan
| # | Task | Output |
|---|---|---|
| 1 | Approve mockup and this spec | decisions below |
| 2 | Repair build: LensService API, registration, delete LensCollector, fix tests/package | green ./gradlew test |
| 3 | Dependency update: BoxLang 1.13.0 to current, Gradle plugins, test libs | dependencyUpdates clean |
| 4 | Core: LensConfig, LensRequest, RequestStore, access guard, redaction | unit tests |
| 5 | Injector + asset endpoint | bar renders in MiniServer |
| 6 | Collectors wave 1: request, timeline, templates, queries, exceptions, messages, timers | per-collector tests |
| 7 | Collectors wave 2: http, scopes, jvm, functions, logs, cache, session, modules | per-collector tests |
| 8 | UI build-out from mockup, History, ajax tracking | manual pass in MiniServer |
| 9 | Docs, readme, changelog, CI | release candidate |
10. Open items
- Cache hit/miss, SOAP,
onComponentInvocationandpostBIFInvocationneed core changes (section 3.7). Decide: file Jira issues and skip in v1, or fix in core first. - Confirm servlet and CommandBox paths run the same
onRequestEndflow as MiniServer. - Should
Historysurvive an app reinit (global store) or reset with it? - Editor link path mapping for Docker/remote (
remoteBasetolocalBase) is included; confirm it is wanted in v1.