Api Spec

On this page

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.remoteBase and editor.localBase path 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 no injectPosition, no X-BxLens cookie or header override, no collectors.request, timeline or session, and slow thresholds live only under thresholds. Added: limits, collectors.transactions, collectors.logs.max, collectors.queries.captureCaller, collectors.scopes.variables and dev.reloadAssets. collectors.cache is 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 announce onRequestFlushBuffer, 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:lens component.
  • 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

PieceResponsibility
LensService (global service)Owns config, collector registry, RequestStore, GlobalStats.
LensRequestTyped per-request data. Stored as a context attachment on the request context.
ILensCollectorSPI. One class per panel. Java only. Registered as interceptors by the service.
RequestStoreBounded ring buffer of finished LensRequest snapshots (default 50). No disk, no DB.
LensInjectorInterceptor that renders the bar into HTML responses at end of request.
Asset endpointPlanned 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 bxLens logger 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.*)

EventPayloadUse
onRequestStartcontext, args, application, listenercreate LensRequest, access check
onRequestsametemplate/class entry
onRequestEndsamefinalize, store, inject. Skipped on unhandled exceptions and aborts (WebRequestExecutor)
onErrorsame (args holds the exception)exception capture, finalize and inject on error pages
onAbortsamefinalize and inject on abort
onMissingTemplatesame404 issue
onSessionStart, onSessionEndsamesession panel
onRequestFlushBuffercontext, output (mutable)alternative injection point, fires on every flush so only the last flush may be modified
onApplicationStart/End/Restart/Defined, beforeApplicationListenerLoad, afterApplicationListenerLoadapp dataApp panel
onWebExecutorRequest (web-support)context, appListener, requestString, exchange, updatedRequestroute rewrites, request identity

3.2 Execution

EventPayloadUse
preTemplateInvoke, postTemplateInvokecontext, template, templatePathtemplates/includes tree. No executionTime, we time it ourselves
preFunctionInvoke, postFunctionInvokecontext, arguments, function, name (+ result on post)functions panel (opt in, hot path)
onFunctionExceptionsame + exceptionexceptions
onBIFInvocationcontext, 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, onPostSourceInvokescript/source executioneval/script timing
afterBoxClassCreation, afterBoxClassInit, afterDynamicObjectCreation, onCreateObjectRequestobject createdObjects panel (opt in)
onComponentInstance, onBIFInstancedescriptorstartup only
onComponentInvocationdeclared in BoxEvent, never announced by corecannot be used today. Needs a core change for a Components panel

3.3 Data

EventPayloadUse
onQueryBuildquery build databuilder usage
preQueryExecutesql, bindings, pendingQuery, contextstart time, SQL text
postQueryExecutesql, bindings, executionTime, data, result (meta), pendingQuery, executedQuery, contextqueries panel; row count from data, datasource from pendingQuery
queryAddRowquery rowignore
onTransactionBegin/Acquire/Commit/Rollback/SetSavepoint/Release/Endconnection, transaction, contexttransactions panel, shown as spans on the waterfall
onDatasourceConfigLoad, onDatasourceStartup, onDatasourceInitialized, service start/stopdatasourceJVM/Runtime panel
afterCacheElementInsert/Updated/Removed, beforeCacheElementRemoved, afterCacheClearAllcache elementcache writes only. No read/hit/miss event
onCacheComponentActioncache component actionoutput cache usage

3.4 I/O and misc

EventPayloadUse
onHTTPRequestresult, httpClient, httpRequestHTTP panel start
onHTTPRawResponse, onHTTPResponseresult, response, httpClient, httpRequeststatus, size, time
onHTTPErrorerror dataissue
onFileComponentActionfile actionFiles panel (opt in)
logMessagetext, log, typeLogs panel
onBXDump / onMissingDumpOutputdump dataroute bx:dump output into Messages instead of the page body
beforeObjectMarshallSerialize etc.serializationignore

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

  1. postBIFInvocation is never announced (bug).
  2. onComponentInvocation is never announced.
  3. No cache read/hit/miss event.
  4. No SOAP events.
  5. 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

NameSignatureNotes
lensMessage( message, label="info" )label: info, warn, error, debug or any string. Structs/arrays are serialized.
lensStart( label ) : idStarts a timer.
lensStop( labelOrId )Ends a timer. Unmatched labels are ignored.
lensMeasure( label, callable ) : anyTimes 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() : stringManual render when inject=false.
lensIsEnabled() : booleanNew.
lensDump( value, label="" )New. Pushes a value to the Messages panel with a collapsible tree.
bx:lensaction="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 onRequestEnd with fallbacks on onError and onAbort. Pattern follows web-support HtmlBody.
  • Inject only when: lens enabled, caller allowed, response Content-Type starts with an entry in contentTypes, status is not a redirect, response not already committed, request not matched by excludePaths.
  • 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.trackNonHtml is on (default), the request is still collected and stored, and the id is returned in the X-BxLens-Id header 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.

  1. bx-lens registers its own interception points in configure() of ModuleConfig.bx:
PointWhenPayload
onLensRegisterat bx-lens activation and again on postModuleLoadregistry with registry.panel( id, label, icon, order, renderer )
onLensRequestStartper request, after LensRequest existscontext, requestId
onLensCollectper request, before rendercontext, requestId, panel( id ) handle to add data
onLensRequestFinishper request, after storerequestId, summary
  1. 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" );
}
  1. App code can use the same thing without a module through BIFs: lensPanel( id, label ).table( ... ), .kv( ... ), .tree( ... ), .spans( ... ), .messages( ... ), .json( ... ).

  2. 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.

  3. Contributions can add Issues (panel.issue( severity, title, detail, file, line )) which show on the Issues tab and drive the strip color.

  4. 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

#TaskOutput
1Approve mockup and this specdecisions below
2Repair build: LensService API, registration, delete LensCollector, fix tests/packagegreen ./gradlew test
3Dependency update: BoxLang 1.13.0 to current, Gradle plugins, test libsdependencyUpdates clean
4Core: LensConfig, LensRequest, RequestStore, access guard, redactionunit tests
5Injector + asset endpointbar renders in MiniServer
6Collectors wave 1: request, timeline, templates, queries, exceptions, messages, timersper-collector tests
7Collectors wave 2: http, scopes, jvm, functions, logs, cache, session, modulesper-collector tests
8UI build-out from mockup, History, ajax trackingmanual pass in MiniServer
9Docs, readme, changelog, CIrelease candidate

10. Open items

  1. Cache hit/miss, SOAP, onComponentInvocation and postBIFInvocation need core changes (section 3.7). Decide: file Jira issues and skip in v1, or fix in core first.
  2. Confirm servlet and CommandBox paths run the same onRequestEnd flow as MiniServer.
  3. Should History survive an app reinit (global store) or reset with it?
  4. Editor link path mapping for Docker/remote (remoteBase to localBase) is included; confirm it is wanted in v1.