miradb 0.2.0

Mira: an OTLP-native telemetry storage engine in a single binary
<script>
  import { api, bounds, fmtDur, layer, num } from './api.js'
  import { go } from './route.svelte.js'

  let { params, nonce, onstats } = $props()

  let graph = $state(null)
  let loading = $state(true)
  let error = $state('')

  $effect(() => {
    const range = params.range || '-1h'
    void nonce
    let live = true
    loading = true
    error = ''
    ;(async () => {
      try {
        const r = await api('/api/v1/map', bounds(range))
        if (!live) return
        graph = r.map
        onstats(r.stats)
      } catch (e) {
        if (live) { error = String(e.message || e); graph = null; onstats(null) }
      } finally {
        if (live) loading = false
      }
    })()
    return () => { live = false }
  })

  // Columns from `layer`, rows by arrival. Deliberately not force-directed:
  // those move every node when one edge changes, and this page refreshes on a
  // timer — a graph that reshuffles itself every three seconds cannot be read.
  const ROW = 62
  const H = 34
  const GAP = 68

  const sub = (n) => `${n.spans} spans · ${n.errors} err · ${fmtDur(n.avg_nano)}`

  // The box is sized from its longest line rather than fixed, because SVG text
  // neither wraps nor clips: a box too narrow for its content does not truncate
  // it, it draws it over the edge leaving the node and over the node it arrives
  // at. Monospace is what makes the width knowable without measuring — 12px
  // `ui-monospace` advances 0.6em — and the 18 is the two 9px gutters.
  const CH = 7.3
  let W = $derived(
    Math.max(
      132,
      ...(graph?.nodes || []).map((n) => 18 + CH * Math.max(n.name.length, sub(n).length)),
    ),
  )
  let COL = $derived(W + GAP)

  let laid = $derived.by(() => {
    if (!graph?.nodes.length) return null
    // `entry` is a synthetic node (frame.rs) and belongs on screen: a map that
    // hides where traffic arrives cannot be read left to right.
    const nodes = [{ key: 'entry', name: 'entry', spans: 0, errors: 0 }, ...graph.nodes]
    const depth = layer(nodes, graph.edges)
    const cols = new Map()
    for (const n of nodes) {
      const d = depth.get(n.key)
      if (!cols.has(d)) cols.set(d, [])
      cols.get(d).push(n)
    }
    const at = new Map()
    for (const [d, list] of cols) {
      list.forEach((n, i) => at.set(n.key, { x: 20 + d * COL, y: 20 + i * ROW, n }))
    }
    const rows = Math.max(...[...cols.values()].map((l) => l.length))
    return {
      at,
      placed: [...at.values()],
      width: 40 + (cols.size - 1) * COL + W,
      height: 40 + (rows - 1) * ROW + H,
    }
  })

  // Thickness by call volume on a log scale: a 10,000-call edge next to a
  // 3-call one is unreadable linearly, and the question the width answers is
  // "which way does the traffic mostly go", not "how much exactly".
  const busiest = $derived(Math.max(1, ...(graph?.edges || []).map((e) => e.calls)))
  const width = (e) => 1 + 4 * (Math.log1p(e.calls) / Math.log1p(busiest))

  function path(e) {
    const a = laid.at.get(e.from)
    const b = laid.at.get(e.to)
    if (!a || !b) return ''
    const [x0, y0] = [a.x + W, a.y + H / 2]
    const [x1, y1] = [b.x, b.y + H / 2]
    // A backward edge would otherwise be drawn straight through the boxes
    // between its endpoints; bowing it out below is enough to follow by eye.
    const bow = x1 <= x0 ? Math.max(30, (y1 - y0) / 2 + 40) : 0
    const mx = (x0 + x1) / 2
    return `M${x0},${y0} C${mx + bow},${y0 + bow} ${mx - bow},${y1 + bow} ${x1},${y1}`
  }

  const rate = (e) => (e.calls ? e.errors / e.calls : 0)
  const pct = (v) => (v * 100).toFixed(v >= 0.1 ? 0 : 1) + '%'

  // Clicking a service is the point of the map: it is how you get from "the
  // graph says checkout is red" to the log lines that say why.
  const drill = (n) =>
    go('/logs', { q: `service.name="${n.name}"`, range: params.range || '-1h' })
</script>

{#if error}
  <div class="err">{error}</div>
{:else if loading && !graph}
  <div class="empty">Loading…</div>
{:else if !laid}
  <div class="empty">
    No spans in this window. The map is built from <code>parent_span_id</code> at
    read time, so it needs traces — send some and it appears.
  </div>
{:else}
  <div class="bar">
    {graph.nodes.length} services · {graph.edges.length} edges
    {#if num(graph.unresolved) > 0}
      <!-- An unresolved span is one whose parent was not in the sample, so
           every edge on screen is a lower bound. Saying how many is the only
           way a reader can tell a thin dependency from a truncated read. -->
      <span class="warn">
        · {graph.unresolved} spans with a parent outside the sample
      </span>
    {/if}
  </div>
  <svg viewBox="0 0 {laid.width} {laid.height}" style="height:{laid.height}px">
    <defs>
      <marker id="a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="6"
              markerHeight="6" orient="auto-start-reverse">
        <path d="M0,0 L8,4 L0,8 z" fill="var(--dim)" />
      </marker>
    </defs>
    {#each graph.edges as e (e.from + '>' + e.to)}
      <path
        d={path(e)}
        class="edge"
        class:bad={rate(e) > 0}
        stroke-width={width(e)}
        marker-end="url(#a)"
      >
        <title>{e.calls} calls · {e.errors} errors ({pct(rate(e))}) · avg {fmtDur(e.avg_nano)} · max {fmtDur(e.max_nano)}</title>
      </path>
    {/each}
    {#each laid.placed as { x, y, n } (n.key)}
      <g
        class="node"
        class:entry={n.key === 'entry'}
        transform="translate({x},{y})"
        role="button"
        tabindex="0"
        onclick={() => n.key !== 'entry' && drill(n)}
        onkeydown={(ev) => {
          if (ev.key === 'Enter' && n.key !== 'entry') drill(n)
        }}
      >
        <rect width={W} height={H} rx="6" />
        <text x="9" y="14">{n.name}</text>
        {#if n.key !== 'entry'}
          <text x="9" y="27" class="sub" class:bad={n.errors > 0}>{sub(n)}</text>
          <title>{n.name} — click to see its logs</title>
        {/if}
      </g>
    {/each}
  </svg>
{/if}

<style>
  .bar { padding: 12px 0 4px; color: var(--dim); }
  .warn { color: var(--warn); }
  svg { width: 100%; display: block; }
  .edge { fill: none; stroke: var(--dim); opacity: 0.7; }
  .edge.bad { stroke: var(--err); opacity: 1; }
  .node rect { fill: var(--panel); stroke: var(--line); }
  .node { cursor: pointer; }
  .node.entry { cursor: default; }
  .node.entry rect { fill: none; stroke-dasharray: 3 3; }
  .node:not(.entry):hover rect { stroke: var(--accent); }
  .node:focus-visible { outline: none; }
  .node:focus-visible rect { stroke: var(--accent); stroke-width: 2; }
  text { fill: var(--fg); font: 12px ui-monospace, monospace; }
  .sub { fill: var(--dim); font-size: 10px; }
  .sub.bad { fill: var(--err); }
</style>