v1.6.0

<bmx-log-stream>

A log and audit viewer for the browser: millions of lines of server logs, build output, audit trails or CSV exports, read from a file, a URL, a stream or live appends, and searched, filtered and coloured entirely on the page - no log server, nothing sent anywhere.

30 properties · 6 events · 14 methods · 35 parts

Example

level:>=warn status:>=500 Clear
Everything runs in the browser. The log below is made in this page and opened as a file would be; drop one of your own on the viewer to read it instead. Search with words, field:value or /regular expressions/, hide levels with the chips, select a line and press Enter for its fields, and use Export to save what is on show. Email addresses are masked.
Show markup
<div class="row" role="group" aria-label="Example queries">
  <bmx-button id="ex-ls-errors" variant="soft">level:&gt;=warn</bmx-button>
  <bmx-button id="ex-ls-5xx" variant="soft">status:&gt;=500</bmx-button>
  <bmx-button id="ex-ls-clear" variant="ghost">Clear</bmx-button>
</div>

<bmx-log-stream id="ex-ls" label="Order service log" redact="email" style="--bmx-log-stream-height: 24rem; margin-block-start: 1rem"></bmx-log-stream>

<div class="row" style="margin-block-start: 1rem">
  <span class="note" id="ex-ls-out">
    <strong>Everything runs in the browser.</strong> The log below is made in this page and opened as a file would be;
    drop one of your own on the viewer to read it instead. Search with words, <code>field:value</code> or
    <code>/regular expressions/</code>, hide levels with the chips, select a line and press <kbd>Enter</kbd> for its
    fields, and use <strong>Export</strong> to save what is on show. Email addresses are masked.
  </span>
</div>

<script type="module">
  await customElements.whenDefined('bmx-log-stream');

  const log = document.getElementById('ex-ls');
  const out = document.getElementById('ex-ls-out');

  // Two thousand JSON lines: an order service, with a stack trace now and then.
  const paths = ['/api/orders', '/api/cart', '/api/checkout', '/api/session'];
  const lines = [];
  for (let i = 0; i < 2000; i += 1) {
    const time = new Date(Date.UTC(2026, 8, 28, 9) + i * 1800).toISOString();
    const status = i % 97 === 0 ? 503 : i % 23 === 0 ? 404 : 200;
    const level = status >= 500 ? 'error' : status >= 400 ? 'warn' : i % 5 === 0 ? 'debug' : 'info';
    lines.push(JSON.stringify({ time, level, msg: `GET ${paths[i % 4]}`, status, ms: (i * 37) % 400, email: `customer${i % 40}@example.com` }));
  }
  await log.load(new Blob([lines.join('\n')], { type: 'application/x-ndjson' }));

  const queries = { 'ex-ls-errors': 'level:>=warn', 'ex-ls-5xx': 'status:>=500', 'ex-ls-clear': '' };
  for (const [id, query] of Object.entries(queries)) {
    document.getElementById(id).addEventListener('click', () => (log.query = query));
  }

  // Events: what is on show, and the line the reader chose.
  log.addEventListener('bmxLogFilter', event => {
    const { matches, total } = event.detail;
    out.textContent = `bmxLogFilter - ${matches} of ${total} lines match`;
  });
  log.addEventListener('bmxLogSelect', event => {
    out.textContent = `bmxLogSelect - line ${event.detail.line.number}: ${JSON.stringify(event.detail.fields)}`;
  });

  // Live lines are appended as they arrive, from a WebSocket for example:
  //   socket.onmessage = message => log.appendLog(message.data);
</script>

READING

The format is detected from the first lines - JSON lines, logfmt, syslog, Apache and Nginx access logs, CSV and TSV, or plain text - or set with format, or given as a regular expression with named groups in pattern. Every line gets its level and time; terminal colour codes are drawn as colours; a stack trace stays with the line it belongs to and folds away.

FINDING

The search box takes words, "phrases", /regular expressions/, level:error, status:>=500, -exclusions, OR and brackets. Level chips show and hide each level with its count, context shows the entries around each match, and every match is highlighted.

The timeline counts the entries on show over time, errors and warnings in their own colours; drag across it, or choose with the arrow keys and Enter, to narrow to that stretch of time. The fields panel lists each field's commonest values: choose one to filter by it. columns lays structured lines out as a table.

LIVE

connect() follows a WebSocket or a server-sent event stream, and opens it again when it drops. Bookmarks mark lines to come back to (M, then ] and [ to step through them), and getState() packs the whole view into a string for a link that reopens it.

SAFE

Log text is always drawn as text, never as markup, so a line carrying <script> or <img onerror> shows those characters and runs nothing. redact masks emails, IP addresses, tokens, keys and card numbers on screen, in search and in exports.

LARGE

Lines are kept compactly and only the rows in view are drawn; loading runs in slices, so the page never freezes. A log of fifty thousand lines or more is searched on a background thread (a worker started from a Blob, so a Content Security Policy needs worker-src blob:), and typing in the search box stays smooth; without a worker, or past 128 million characters, the search runs here in slices instead and finds the same lines.

Properties

PropertyAttributeTypeDefaultDescription
bookmarks property only number[] [] Bookmarked line numbers.
bookmarksOnly bookmarks-only boolean false Show only the bookmarked entries.
caseSensitive case-sensitive boolean false Match the search's letter case.
columns columns string '' Lay the lines out as columns: auto, or field names separated by spaces. @time, @level and @message are the line's own. Empty shows whole lines.
context context number 0 Entries to show either side of each match.
dropFiles drop-files boolean true Open a file dropped on the viewer.
fieldsPanel fields-panel boolean false Show the fields panel: each field's commonest values, to filter by.
fileName file-name string — The name exported files are saved under.
follow follow boolean false Keep the newest line in view as lines arrive. Scrolling up turns it off.
format format BmxLogFormat 'auto' The format of the lines. auto reads it from the first lines.
label label string — The accessible name. Default: "Log".
levels levels string | readonly string[] '' Levels to show, separated by spaces: warn error fatal; none for lines with no level. Empty shows all.
lineNumbers line-numbers boolean true Show line numbers.
liveSrc live-src string — A WebSocket (ws:, wss:) or server-sent events URL to follow live.
liveType live-type 'auto' | 'websocket' | 'sse' 'auto' What liveSrc is. auto goes by the URL's scheme.
maxLines max-lines number 0 Keep at most this many lines, dropping the oldest: for live logs. 0 keeps all.
pattern pattern string — For format="pattern": a regular expression with named groups; level, time and message are understood.
query query string '' The search: words, "phrases", /regex/, field:value, -exclusions, OR.
redact redact boolean | string false Mask personal data and secrets: true for every rule, or rule names: email ip card jwt bearer aws-key secret.
redactRules property only readonly BmxLogRedactRule[] — Masking rules of your own: { name, pattern, flags?, mask? }.
regex regex boolean false Treat the search as one regular expression.
requestInit property only RequestInit — Options for fetching src: headers, credentials.
src src string — A URL to read the log from, streamed as it downloads. A .gz file is decompressed.
timeFrom time-from string | number — Show entries from this time on: an ISO date or milliseconds since 1970. Set by the timeline.
timeTo time-to string | number — Show entries before this time. Set by the timeline.
timeZone time-zone 'utc' | 'local' 'utc' Show times in UTC or in the reader's own time zone.
timeline timeline boolean true Show the timeline over the lines, when they carry times.
toolbar toolbar boolean true Show the toolbar.
wholeWord whole-word boolean false Match the search's words only as whole words.
wrap wrap boolean false Wrap long lines instead of scrolling sideways.

Events

EventDetailDescription
bmxLogBookmark BmxLogBookmarkDetail A bookmark was added or removed.
bmxLogConnection BmxLogConnectionDetail A live connection opened, dropped, is being tried again, or closed.
bmxLogFilter BmxLogFilterDetail The lines on show changed.
bmxLogLoad BmxLogLoadDetail Text arrived, or the load finished or failed.
bmxLogSelect BmxLogSelectDetail A line was chosen: clicked, or reached by the keyboard.
bmxLogStateChange BmxLogStateChangeDetail The search, levels, time range, bookmarks, columns or chosen line changed.

Methods

MethodSignatureDescription
appendLog appendLog(text: string | readonly string[]) => Promise<number> Adds text or lines, as from a live source. Text without a final line break waits for the rest.
clear clear() => Promise<void> Forgets every line.
connect connect(url: string, options?: BmxLogConnectOptions) => Promise<void> Follows a live source: a WebSocket (ws:, wss:) or a server-sent events URL. Each message is one or more lines. The connection is opened again when it drops; bmxLogConnection reports each change.
disconnect disconnect() => Promise<void> Closes the live connection. The lines already received stay.
download download(format?: BmxLogExportFormat, fileName?: string) => Promise<void> Saves the lines on show, under fileName or log.
exportAs exportAs(format?: BmxLogExportFormat) => Promise<Blob> The lines on show as a file: text, csv or json, masked as on screen.
findNext findNext() => Promise<number | null> Moves to the next match. Resolves to its line number, or null when there is none.
findPrevious findPrevious() => Promise<number | null> Moves to the previous match.
getLine getLine(lineNumber: number) => Promise<(BmxLogLine & { fields: Record<string, unknown> | null; }) | null> A line by its number, or null when it is not held.
getState getState() => Promise<string> What the reader is looking at, as a short URL-safe string: the search, levels, context, time range, bookmarks, columns and the chosen line. Keep it in a link and give it to setState() to open the same view.
getStats getStats() => Promise<BmxLogStats> Counts: lines held and shown, matches, characters, and entries per level.
load load(source: BmxLogSource) => Promise<void> Reads a log, replacing what is shown: a File or Blob (a dropped or chosen file), a URL, a Response, or a stream. Gzip is recognised and decompressed.
scrollToLine scrollToLine(lineNumber: number) => Promise<boolean> Scrolls to a line by its number, selects it, and says whether it is on show.
setState setState(state: string) => Promise<boolean> Opens a view saved by getState(). Call it once the log is loaded, so the chosen line can be found. False when the string is not a saved view.

Slots

SlotDescription
empty what to show when there are no lines.
toolbar-end extra controls at the end of the toolbar.

CSS shadow parts

PartDescription
body
button
cell
columns-head
context
details
drop-zone
empty
error
export-list
export-option
field
field-value
fields
fields-panel
fold
latest
level
level-chip
level-chip-bookmarks
level-chip-none
line-number
match
match-count
option
query-error
raw
search
search-input
status
text
timeline
timeline-chart
toolbar
viewport

CSS custom properties

PropertyDescription
--bmx-log-stream-active The chosen line.
--bmx-log-stream-ansi-0 Terminal black. -1 to -7 are red, green, yellow, blue, magenta, cyan and white; -8 to -15 their bright forms.
--bmx-log-stream-ansi-1 Terminal red.
--bmx-log-stream-ansi-10 Terminal bright green.
--bmx-log-stream-ansi-11 Terminal bright yellow.
--bmx-log-stream-ansi-12 Terminal bright blue.
--bmx-log-stream-ansi-13 Terminal bright magenta.
--bmx-log-stream-ansi-14 Terminal bright cyan.
--bmx-log-stream-ansi-15 Terminal bright white. @part toolbar - the controls above the lines. @part button - a toolbar button. @part search - the search group. @part search-input - the search field. @part option - the match case, whole word and regular expression toggles. @part match-count - "3 of 12". @part query-error - what is wrong with the search. @part context - the context control. @part export-list - the list of file types under Export. @part export-option - one of them. @part level-chip - a level's toggle. Also level-chip-error and so on, and level-chip-bookmarks. @part timeline - the timeline over the lines. @part timeline-chart - its bars, which choose a stretch of time. @part fields-panel - the fields panel. @part field - one field in it. @part field-value - one of its values, which filters by it. @part columns-head - the column titles, in the column layout. @part cell - one column of a line. @part body - the lines and what lies over them. @part viewport - the scrolling list of lines. @part line-number - a line's number. @part level - a line's level mark. @part fold - the button that folds an entry's following lines. @part text - a line's text. @part match - a search match. @part latest - the "new lines" button while not following. @part drop-zone - shown while a file is dragged over. @part details - the chosen line's fields and text. @part fields - its fields. @part raw - its text. @part status - the counts under the lines. @part empty - what is shown when there are no lines. @part error - the message when a log could not be read.
--bmx-log-stream-ansi-2 Terminal green.
--bmx-log-stream-ansi-3 Terminal yellow.
--bmx-log-stream-ansi-4 Terminal blue.
--bmx-log-stream-ansi-5 Terminal magenta.
--bmx-log-stream-ansi-6 Terminal cyan.
--bmx-log-stream-ansi-7 Terminal white.
--bmx-log-stream-ansi-8 Terminal bright black.
--bmx-log-stream-ansi-9 Terminal bright red.
--bmx-log-stream-background Behind the lines.
--bmx-log-stream-debug The debug level's colour.
--bmx-log-stream-error The error level's colour.
--bmx-log-stream-fatal The fatal level's colour.
--bmx-log-stream-font The lines' font. A monospace font.
--bmx-log-stream-font-size The lines' size.
--bmx-log-stream-height How tall the viewer is.
--bmx-log-stream-info The info level's colour.
--bmx-log-stream-match A search match.
--bmx-log-stream-text The lines' text.
--bmx-log-stream-trace The trace level's colour. Also -debug, -info, -warn, -error, -fatal.
--bmx-log-stream-warn The warning level's colour.