Skip to content

Commit

Permalink
Add span timelines
Browse files Browse the repository at this point in the history
  • Loading branch information
benbrandt committed Oct 27, 2023
1 parent 6fb9eb4 commit 21dffce
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 15 deletions.
26 changes: 26 additions & 0 deletions log-viewer/src/lib/components/timeline/LogDurations.stories.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script context="module" lang="ts">
import type { MetaProps } from '@storybook/addon-svelte-csf';
import { randomLogger } from '../../log.test_utils';
import LogDurations from './LogDurations.svelte';
const logger = randomLogger();
export const meta: MetaProps = {
title: 'Timeline/Molecules/LogDurations',
component: LogDurations,
tags: ['autodocs'],
args: {
logs: logger.logs
}
};
</script>

<script lang="ts">
import { Story, Template } from '@storybook/addon-svelte-csf';
</script>

<Template let:args>
<LogDurations {...args} />
</Template>

<Story name="Base" />
35 changes: 35 additions & 0 deletions log-viewer/src/lib/components/timeline/LogDurations.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script lang="ts">
import { compareAsc } from 'date-fns';
import { type Entry, isSpan } from '../../log';
import SpanDuration from './SpanDuration.svelte';
/**
* The list of log entries to render in the tree.
*/
export let logs: Entry[];
// Filter out LogEntry's, only show the Span/TaskSpan in the tree
$: spans = logs.filter(isSpan);
let logTimes: Date[];
$: {
logTimes = logs.reduce<Date[]>((acc, i) => {
if ('timestamp' in i) acc.push(new Date(i.timestamp));
if ('start_timestamp' in i) acc.push(new Date(i.start_timestamp));
if ('end_timestamp' in i) acc.push(new Date(i.end_timestamp));
return acc;
}, []);
logTimes.sort(compareAsc);
console.log(logTimes);
}
</script>

<!--
@compone nt
A timeline of all sub-span durations
-->
<div class="w-full">
{#each spans as span}
<SpanDuration {span} runStart={logTimes[0]} runEnd={logTimes[logTimes.length - 1]} />
{/each}
</div>
29 changes: 29 additions & 0 deletions log-viewer/src/lib/components/timeline/SpanDuration.stories.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script context="module" lang="ts">
import type { MetaProps } from '@storybook/addon-svelte-csf';
import { randomSpan, randomDateRange } from '../../log.test_utils';
import SpanDuration from './SpanDuration.svelte';
const range = randomDateRange();
const span = randomSpan(range);
export const meta: MetaProps = {
title: 'Timeline/Atoms/SpanDuration',
component: SpanDuration,
tags: ['autodocs'],
args: {
span,
runStart: range.from,
runEnd: range.to
}
};
</script>

<script lang="ts">
import { Story, Template } from '@storybook/addon-svelte-csf';
</script>

<Template let:args>
<SpanDuration {...args} />
</Template>

<Story name="Base" />
64 changes: 64 additions & 0 deletions log-viewer/src/lib/components/timeline/SpanDuration.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<script lang="ts">
import { differenceInMilliseconds } from 'date-fns';
import { type SpanEntry, isSpan } from '../../log';
/**
* A Span or TaskSpan to show the duration of
*/
export let span: SpanEntry;
/**
* The start of the entire run of the logger
*/
export let runStart: Date;
/**
* The end of the entire run of the logger
*/
export let runEnd: Date;
$: spanStart = new Date(span.start_timestamp);
$: spanOffset = differenceInMilliseconds(spanStart, runStart);
$: spanLength = differenceInMilliseconds(new Date(span.end_timestamp), spanStart);
$: runLength = differenceInMilliseconds(runEnd, runStart);
// Filter out LogEntry's, only show the Span/TaskSpan in the tree
$: childSpans = span.logs.filter(isSpan);
function renderDuration(spanLength: number): string {
let unit = 'ms';
let length = spanLength;
if (length > 1000) {
length /= 1000;
unit = 's';
if (length > 60) {
length /= 60;
unit = 'min';
if (length > 60) {
length /= 60;
unit = 'h';
}
}
}
return `${length.toLocaleString('en-US')}${unit}`;
}
</script>

<!--
@component
A view of Span durations in relation to the entire duration of the log.
This is a recursive component that builds itself up by creating the same component for sub-spans.
-->
<div class="w-full py-0.5 hover:bg-gray-50">
<button
class="h-8 bg-accent-400 py-1 text-right text-xs font-extrabold text-gray-950 shadow outline-none ring-1 ring-gray-950/20 hover:bg-accent-500"
style="margin-left: {Math.round((spanOffset / runLength) * 100)}%; width:{Math.round(
(spanLength / runLength) * 100
)}%;"
>
<span class="px-1">{renderDuration(spanLength)}</span>
</button>
</div>
{#each childSpans as span}
<svelte:self {span} {runStart} {runEnd} />
{/each}
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
<script context="module" lang="ts">
import type { MetaProps } from '@storybook/addon-svelte-csf';
import type { Entry } from '../../log';
import { randomSpan } from '../../log.test_utils';
import { randomLogger } from '../../log.test_utils';
import SpanTree from './SpanTree.svelte';
const logs: Entry[] = [randomSpan()];
const logger = randomLogger();
export const meta: MetaProps = {
title: 'Timeline/Molecules/SpanTree',
title: 'Timeline/Atoms/SpanTree',
component: SpanTree,
tags: ['autodocs'],
args: {
logs
logs: logger.logs
}
};
</script>
Expand Down
11 changes: 9 additions & 2 deletions log-viewer/src/lib/components/timeline/SpanTree.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,20 @@
<ul class:border-t={level === 0}>
{#each spans as log}
<li>
<button class="group w-full border-b text-left" style="padding-left: {level}em">
<span class="block border-l border-gray-300 px-2 py-1 text-sm group-hover:bg-gray-50">
<button
class="group h-8 w-full border-b bg-gray-50 text-left"
style="padding-left: {level}em"
>
<span
class="block border-l border-gray-300 bg-white px-2 py-1 text-sm group-hover:bg-gray-100"
>
{log.name}
</span>
</button>
<svelte:self logs={log.logs} level={level + 1} />
</li>
{/each}
</ul>
{:else if level === 0}
<p class="text-sm">No spans available</p>
{/if}
24 changes: 16 additions & 8 deletions log-viewer/src/lib/log.test_utils.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { faker } from '@faker-js/faker';
import { compareAsc } from 'date-fns';
import type { Entry, LogEntry, Span, TaskSpan, JSONValue } from './log';
import type { Entry, LogEntry, Span, TaskSpan, JSONValue, DebugLog } from './log';

interface TimeRange {
export interface TimeRange {
from: Date;
to: Date;
}

/**
* Produces a random time range, with optional time bounds
*/
function randomDateRange(between?: TimeRange): TimeRange {
export function randomDateRange(between?: TimeRange): TimeRange {
if (between) {
const dates = faker.date.betweens({ ...between, count: 2 });
dates.sort(compareAsc);
Expand All @@ -22,16 +22,16 @@ function randomDateRange(between?: TimeRange): TimeRange {
return { from: faker.date.recent({ refDate: end }), to: end };
}

function randomValue(): JSONValue {
export function randomValue(): JSONValue {
return faker.helpers.arrayElement([
faker.word.sample,
faker.number.int,
faker.datatype.boolean,
() => faker.word.sample(),
() => faker.number.int(),
() => faker.datatype.boolean(),
() => null,
() => faker.helpers.multiple(randomValue, { count: { max: 2, min: 0 } }),
() =>
faker.helpers
.multiple(faker.word.sample, { count: { max: 2, min: 0 } })
.multiple(() => faker.word.sample(), { count: { max: 2, min: 0 } })
.reduce((acc, key) => ({ ...acc, [key]: randomValue() }), {})
])();
}
Expand Down Expand Up @@ -78,3 +78,11 @@ export function randomEntry(between?: TimeRange): Entry {
() => randomTaskSpan(between)
])();
}

export function randomLogger(): DebugLog {
const range = randomDateRange();
return {
name: faker.word.sample(),
logs: faker.helpers.multiple(() => randomEntry(range), { count: { max: 2, min: 1 } })
};
}

0 comments on commit 21dffce

Please sign in to comment.