* Improve workers page * Update group config drawer * improve dirty workergroup config * Make layout reactive * fix section animation * prevent opening dropdown while clicking New group config * migrate workers page to svelte 5 * Open drawer upon adding a worker group * nit critical alert table * improve queue metrics drawer * improve agent worker drawer * harmonize copy icon * improve agent worker doc * improve layout * Improve autoscaling event list * Improve tags managment * Remove default tags * fix npm check * Add info for agent workers * improve agent worker jwt token creation * Improve token display * nit * improve tag display * create EE component * nit * harmonize tag overflow * handle permission better * improve env var presets * handle permission for config * nit alerts * nit * Improve custom tag creation in tag select * optimistic tag addition * nit * nit * fix typo * improve workers table * Group config tags * show mismatch * fix typo * optimistic update when adding tag * do not allow to create tag when picking a tag to watch in alerts
276 lines
6.4 KiB
Svelte
276 lines
6.4 KiB
Svelte
<script lang="ts">
|
|
import 'chartjs-adapter-date-fns'
|
|
import { Line } from '$lib/components/chartjs-wrappers/chartJs'
|
|
|
|
import {
|
|
Chart as ChartJS,
|
|
Title,
|
|
Tooltip,
|
|
Legend,
|
|
LineElement,
|
|
CategoryScale,
|
|
LinearScale,
|
|
PointElement,
|
|
LogarithmicScale,
|
|
TimeScale,
|
|
type ChartData,
|
|
type Point
|
|
} from 'chart.js'
|
|
import { WorkerService } from '$lib/gen'
|
|
import Skeleton from './common/skeleton/Skeleton.svelte'
|
|
import DarkModeObserver from './DarkModeObserver.svelte'
|
|
import Alert from './common/alert/Alert.svelte'
|
|
import { Section } from './common'
|
|
|
|
let loading: boolean = true
|
|
|
|
const colorTuples = [
|
|
['#7EB26D', 'rgba(126, 178, 109, 0.2)'],
|
|
['#EAB839', 'rgba(234, 184, 57, 0.2)'],
|
|
['#6ED0E0', 'rgba(110, 208, 224, 0.2)'],
|
|
['#EF843C', 'rgba(239, 132, 60, 0.2)'],
|
|
['#E24D42', 'rgba(226, 77, 66, 0.2)'],
|
|
['#1F78C1', 'rgba(31, 120, 193, 0.2)'],
|
|
['#BA43A9', 'rgba(186, 67, 169, 0.2)'],
|
|
['#705DA0', 'rgba(112, 93, 160, 0.2)'],
|
|
['#508642', 'rgba(80, 134, 66, 0.2)'],
|
|
['#CCA300', 'rgba(204, 163, 0, 0.2)'],
|
|
['#447EBC', 'rgba(68, 126, 188, 0.2)'],
|
|
['#C15C17', 'rgba(193, 92, 23, 0.2)'],
|
|
['#890F02', 'rgba(137, 15, 2, 0.2)'],
|
|
['#666666', 'rgba(102, 102, 102, 0.2)'],
|
|
['#44AA99', 'rgba(68, 170, 153, 0.2)'],
|
|
['#6D8764', 'rgba(109, 135, 100, 0.2)'],
|
|
['#555555', 'rgba(85, 85, 85, 0.2)'],
|
|
['#B3B3B3', 'rgba(179, 179, 179, 0.2)'],
|
|
['#008C9E', 'rgba(0, 140, 158, 0.2)'],
|
|
['#6BBA70', 'rgba(107, 186, 112, 0.2)']
|
|
]
|
|
|
|
function getColors(labels: string[]) {
|
|
const colors = labels.map((_, i) => colorTuples[i % colorTuples.length])
|
|
return Object.fromEntries(colors.map((c, i) => [labels[i], c]))
|
|
}
|
|
|
|
ChartJS.register(
|
|
Title,
|
|
Tooltip,
|
|
Legend,
|
|
LineElement,
|
|
LinearScale,
|
|
PointElement,
|
|
CategoryScale,
|
|
TimeScale,
|
|
LogarithmicScale
|
|
)
|
|
|
|
let countData: ChartData<'line', Point[], undefined> | undefined = undefined
|
|
let delayData: ChartData<'line', Point[], undefined> | undefined = undefined
|
|
|
|
let minDate = new Date()
|
|
|
|
let noMetrics = false
|
|
|
|
function fillData(
|
|
data: {
|
|
created_at: string
|
|
value: number
|
|
}[],
|
|
zero = 0
|
|
) {
|
|
// fill holes with 0
|
|
const sorted: typeof data = []
|
|
for (const el of [
|
|
...data,
|
|
{
|
|
created_at: new Date().toISOString(),
|
|
value: zero
|
|
}
|
|
]) {
|
|
const last =
|
|
sorted.length > 0 ? new Date(sorted[sorted.length - 1].created_at).getTime() : undefined
|
|
const currentTs = new Date(el.created_at).getTime()
|
|
if (last && currentTs - last > 1000 * 60 * 2) {
|
|
const numElements = Math.floor((currentTs - last) / (1000 * 30))
|
|
for (let i = 1; i < numElements; i++) {
|
|
sorted.push({
|
|
created_at: new Date(last + i * (1000 * 30)).toISOString(),
|
|
value: zero
|
|
})
|
|
}
|
|
}
|
|
sorted.push(el)
|
|
}
|
|
|
|
// remove high frequency data points for similar values
|
|
const light: typeof sorted = []
|
|
for (const el of sorted) {
|
|
const last = light.length > 0 ? light[light.length - 1] : undefined
|
|
if (
|
|
!last ||
|
|
Math.abs((el.value - last.value) / last.value) > 0.1 ||
|
|
new Date(el.created_at).getTime() - new Date(last.created_at).getTime() > 1000 * 60 * 15
|
|
) {
|
|
light.push(el)
|
|
}
|
|
}
|
|
|
|
return light
|
|
}
|
|
|
|
async function loadMetrics() {
|
|
loading = true
|
|
let metrics = await WorkerService.getQueueMetrics()
|
|
|
|
if (metrics.length == 0) {
|
|
noMetrics = true
|
|
loading = false
|
|
return
|
|
}
|
|
|
|
const labels = metrics
|
|
.map((m) => m.id.slice(12))
|
|
.filter((v, i, a) => a.indexOf(v) === i)
|
|
.sort()
|
|
const labelColors = getColors(labels)
|
|
|
|
countData = {
|
|
datasets: metrics
|
|
.filter((m) => m.id.startsWith('queue_count_'))
|
|
.map((m) => {
|
|
const [color, bgColor] = labelColors[m.id.slice(12)]
|
|
return {
|
|
label: m.id.slice(12),
|
|
backgroundColor: bgColor,
|
|
borderColor: color,
|
|
data: fillData(m.values).map((v) => ({ x: v.created_at as any, y: v.value }))
|
|
}
|
|
})
|
|
}
|
|
|
|
delayData = {
|
|
datasets: metrics
|
|
.filter((m) => m.id.startsWith('queue_delay_'))
|
|
.map((m) => {
|
|
const [color, bgColor] = labelColors[m.id.slice(12)]
|
|
return {
|
|
label: m.id.slice(12),
|
|
borderColor: color,
|
|
backgroundColor: bgColor,
|
|
data: fillData(m.values, 1).map((v) => ({
|
|
x: v.created_at as any,
|
|
y: v.value
|
|
}))
|
|
}
|
|
})
|
|
}
|
|
|
|
minDate = new Date(
|
|
Math.min(
|
|
...countData.datasets
|
|
.map((x) => x.data[0].x)
|
|
.filter((x) => x != null)
|
|
.map((d) => new Date(d).getTime())
|
|
)
|
|
)
|
|
|
|
loading = false
|
|
}
|
|
|
|
loadMetrics()
|
|
|
|
let darkMode = false
|
|
|
|
$: ChartJS.defaults.color = darkMode ? '#ccc' : '#666'
|
|
$: ChartJS.defaults.borderColor = darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'
|
|
</script>
|
|
|
|
<DarkModeObserver bind:darkMode />
|
|
|
|
<Section label="Queue metrics">
|
|
{#if loading}
|
|
<Skeleton layout={[[20]]} />
|
|
{:else if noMetrics}
|
|
<p class="text-secondary">No jobs delayed by more than 3 seconds in the last 14 days</p>
|
|
{:else}
|
|
<div class="flex flex-col gap-4">
|
|
{#if countData}
|
|
<Line
|
|
data={countData}
|
|
options={{
|
|
animation: false,
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: 'Number of delayed jobs per tag (> 3s)'
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
type: 'time',
|
|
min: minDate.toISOString(),
|
|
max: new Date().toISOString()
|
|
},
|
|
y: {
|
|
title: {
|
|
display: true,
|
|
text: 'count'
|
|
}
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
{/if}
|
|
{#if delayData}
|
|
<Line
|
|
data={delayData}
|
|
options={{
|
|
animation: false,
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: 'Queue delay per tag (> 3s)'
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function (context) {
|
|
// @ts-ignore
|
|
if (context.raw.y === 1) {
|
|
return context.dataset.label + ': 0'
|
|
} else {
|
|
// @ts-ignore
|
|
return context.dataset.label + ': ' + context.raw.y
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
type: 'time',
|
|
min: minDate.toISOString(),
|
|
max: new Date().toISOString()
|
|
},
|
|
|
|
y: {
|
|
type: 'logarithmic',
|
|
title: {
|
|
display: true,
|
|
text: 'delay (s)'
|
|
},
|
|
ticks: {
|
|
callback: (value, _) => (value === 1 ? '0' : value)
|
|
}
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
{/if}
|
|
<Alert title="Info">
|
|
Only tags for jobs that have been delayed by more than 3 seconds in the last 14 days are
|
|
included in the graph.
|
|
</Alert>
|
|
</div>
|
|
{/if}
|
|
</Section>
|