miradb 0.2.0

Mira: an OTLP-native telemetry storage engine in a single binary
<script>
  import { addTerm, fmtTime, fmtValue, term } from './api.js'
  import { go, route } from './route.svelte.js'

  // The raw row, everything the engine returned. That is the point: a field
  // missing from this dump is missing from storage, not from the renderer.
  let { row } = $props()
  let entries = $derived(
    Object.entries({ ...omit(row), ...(row.attributes || {}) })
      .filter(([, v]) => v !== null && v !== '' && v !== undefined),
  )
  // Events and links are arrays of objects, so they get their own rendering
  // below rather than being stringified into `[object Object]`.
  function omit({ attributes, events, links, ...rest }) {
    return rest
  }
  const kvs = (a) =>
    Object.entries(a || {})
      .map(([k, v]) => `${k}=${fmtValue(v)}`)
      .join(' ')

  // Every key here is either a root column or an attribute, and `parseFilter`
  // decides which from the same FIELDS table -- so a value clicked out of this
  // pane round-trips into the filter box as the term that finds it again. That
  // is the whole feature: nobody remembers the spelling of
  // `dropped_events_count`, and nobody should have to.
  //
  // Not offered for arrays and kvlists: an OTLP AnyValue of those shapes has no
  // scalar spelling, and a term that cannot match is worse than no term.
  const filterable = (v) => v === null || typeof v !== 'object'

  function filter(k, v) {
    // A waterfall has no filter box of its own; the terms it produces belong to
    // the span list it was opened from.
    const path = route.path.startsWith('/trace/') ? '/traces' : route.path
    go(path, { ...route.params, q: addTerm(route.params.q, term(k, v)) })
  }
</script>

<div class="detail-box">
  {#if row.trace_id}
    <a href="#/trace/{row.trace_id}">→ view trace {row.trace_id}</a>
  {/if}
  {#each entries as [k, v] (k)}
    <pre><b>{k}:</b> {#if filterable(v)}<button
          class="val"
          title="filter on {k}"
          onclick={() => filter(k, v)}>{fmtValue(v)}</button>{:else}{fmtValue(v)}{/if}</pre>
  {/each}
  {#each row.events || [] as e, i (i)}
    <pre class="ev"><b>event {fmtTime(e.time_unix_nano)}</b> {e.name || ''} {kvs(e.attributes)}</pre>
  {/each}
  <!-- A link points out of this trace, which is the whole reason to store it:
       it is the only edge that survives an async boundary. -->
  {#each row.links || [] as l, i (i)}
    <div><a href="#/trace/{l.trace_id}">→ linked trace {l.trace_id}</a> {kvs(l.attributes)}</div>
  {/each}
</div>

<style>
  .detail-box { padding: 6px 0; }
  pre {
    margin: 0;
    white-space: pre-wrap;
    word-break: break-all;
    color: var(--dim);
    font: inherit;
  }
  b { color: var(--fg); font-weight: 500; }
  .ev b { color: var(--warn); }
  /* A button that reads as text until it is worth clicking. Underline on hover
     rather than a border, because a border per value turns the dump into a
     grid and the dump is the thing people came here to read. */
  .val {
    font: inherit;
    color: inherit;
    background: none;
    border: none;
    padding: 0;
    text-align: left;
    cursor: pointer;
  }
  .val:hover, .val:focus-visible { color: var(--accent); text-decoration: underline; }
</style>