v1.2.0
  1. ds
  2. Operations
  3. Log viewer

Log viewer

Levels, timestamps, filters and a live tail.

  1. INFOcheckout-apiPOST /v1/orders 201 142ms order_id=ord_9f2k4x
  2. DEBUGevent-relaypicked 50 events from queue webhooks.outbound lag=1.2s
  3. INFOevent-relaydelivered evt_71ab to https://hooks.acme.in/orders 200 88ms
  4. WARNevent-relaydelivery to https://erp.example.net/hook timed out after 10s, retry 3 of 8 in 4.1s
  5. ERRORevent-relaygiving up on evt_70f2 after 8 attempts: connection refused
    endpoint
    https://erp.example.net/hook
    attempts
    8
    first_attempt
    2026-10-11T08:46:12Z
    trace_id
    4bf92f3577b34da6a3ce929d0e0e4736
  6. INFOcheckout-apiGET /v1/orders/ord_9f2k4x 200 31ms
  7. DEBUGledger-writerbatch committed rows=212 duration=18ms
  8. 1,204 lines hidden by the filter
Tailingap-south-1 · last 15 minutes2,881 lines
Show code
<div class="log">
  <div class="log-bar">
    <div class="seg sm"><button aria-pressed="true">All</button><button>Warn</button><button>Error</button></div>
    <input class="inp sm mono" type="search" value="retry" aria-label="Filter lines" />
    <button class="btn xs on" aria-pressed="true"><span class="status-dot live"></span>Live</button>
  </div>
  <ol class="log-rows" role="log" aria-label="Logs, newest at the bottom">
    <li class="log-row" data-lvl="info"><time>14:32:06.981</time><span class="lvl">INFO</span><span class="src">checkout-api</span><span class="msg">POST /v1/orders 201 142ms order_id=ord_9f2k4x</span></li>
    <li class="log-row" data-lvl="debug"><time>14:32:07.004</time><span class="lvl">DEBUG</span><span class="src">event-relay</span><span class="msg">picked 50 events from queue webhooks.outbound lag=1.2s</span></li>
    <li class="log-row" data-lvl="info"><time>14:32:07.118</time><span class="lvl">INFO</span><span class="src">event-relay</span><span class="msg">delivered evt_71ab to https://hooks.acme.in/orders 200 88ms</span></li>
    <li class="log-row" data-lvl="warn"><time>14:32:07.412</time><span class="lvl">WARN</span><span class="src">event-relay</span><span class="msg">delivery to https://erp.example.net/hook timed out after 10s, <mark>retry</mark> 3 of 8 in 4.1s</span></li>
    <li>
      <details class="log-x" open>
        <summary class="log-row" data-lvl="error"><time>14:32:07.498</time><span class="lvl">ERROR</span><span class="src">event-relay</span><span class="msg">giving up on evt_70f2 after 8 attempts: connection refused</span></summary>
        <dl class="log-fields">
          <dt>endpoint</dt><dd>https://erp.example.net/hook</dd>
          <dt>attempts</dt><dd>8</dd>
          <dt>first_attempt</dt><dd>2026-10-11T08:46:12Z</dd>
          <dt>trace_id</dt><dd>4bf92f3577b34da6a3ce929d0e0e4736</dd>
        </dl>
      </details>
    </li>
    <li class="log-row" data-lvl="info"><time>14:32:07.530</time><span class="lvl">INFO</span><span class="src">checkout-api</span><span class="msg">GET /v1/orders/ord_9f2k4x 200 31ms</span></li>
    <li class="log-row" data-lvl="debug"><time>14:32:07.611</time><span class="lvl">DEBUG</span><span class="src">ledger-writer</span><span class="msg">batch committed rows=212 duration=18ms</span></li>
    <li class="log-gap">1,204 lines hidden by the filter</li>
  </ol>
  <div class="log-foot"><span class="status"><span class="status-dot live"></span>Tailing</span><span>ap-south-1 · last 15 minutes</span><span>2,881 lines</span></div>
</div>

A log stream with the newest line at the bottom, the way a terminal does it. Columns are fixed, so timestamps and levels line up and the eye can run down them. Errors get a red wash across the whole row, warnings only colour their level; debug lines drop back to muted. Search matches are marked in the amber wash. A row with structured fields opens in place to show them.

Wrapping

  1. WARNevent-relaydelivery to https://erp.example.net/hook timed out after 10s, retry 3 of 8 in 4.1s; the endpoint has failed 41% of deliveries in the last hour
Code
<div class="log wrap" style="--log-h:8rem">
  <ol class="log-rows">
    <li class="log-row" data-lvl="warn"><time>14:32:07.412</time><span class="lvl">WARN</span><span class="src">event-relay</span><span class="msg">delivery to https://erp.example.net/hook timed out after 10s, retry 3 of 8 in 4.1s; the endpoint has failed 41% of deliveries in the last hour</span></li>
  </ol>
</div>

By default a long message truncates so every line stays one line. .wrap on the viewer wraps messages instead, keeping the columns.

ClassOnWhat it does
.logdivThe viewer: bar, rows, foot. .wrap wraps long messages. --cols sets the column widths.
.log-bardivToolbar: a level .seg, a filter .inp, and actions pushed right.
.log-rowsolThe scrolling lines. --log-h sets the height (22rem). Use role="log".
.log-rowli, summary<time>, .lvl, .src, .msg. data-lvl: debug, info, warn, error.
.log-xdetailsA row that opens. Its summary is a .log-row; the .log-fields list follows.
.log-fieldsdlStructured fields as key and value, aligned under the message.
.log-gapliLines hidden or skipped, on a hatched row.
mark.logA search match.
.log-footdivLive state, scope and counts.
Do
  • Milliseconds in timestamps, and say the timezone once in the foot or the bar.
  • Keep the scroll pinned to the bottom while tailing; stop following as soon as someone scrolls up, and say so.
  • Show how many lines a filter hides.
Don't
  • Colouring info lines. Most lines are info; colour is for the few that matter.
  • Wrapping by default. Ragged rows are hard to scan.
  • Rendering 100,000 rows. Virtualise past a few thousand.

Accessibility

  • role="log" makes new lines polite announcements. For a fast stream, pause announcements and offer a button to read the latest error.
  • The level is a word in its own column, so colour is never the only signal.