feat(new tool): SLA Computer

Fix #738
This commit is contained in:
sharevb 2024-09-22 12:11:00 +02:00 committed by ShareVB
parent 80e46c9292
commit 0e73e5885c
9 changed files with 488 additions and 10 deletions

View file

@ -1,6 +1,7 @@
import { tool as base64FileConverter } from './base64-file-converter';
import { tool as base64StringConverter } from './base64-string-converter';
import { tool as basicAuthGenerator } from './basic-auth-generator';
import { tool as slaCalculator } from './sla-calculator';
import { tool as pdfSignatureChecker } from './pdf-signature-checker';
import { tool as numeronymGenerator } from './numeronym-generator';
import { tool as macAddressGenerator } from './mac-address-generator';
@ -151,7 +152,12 @@ export const toolsByCategory: ToolCategory[] = [
},
{
name: 'Measurement',
components: [chronometer, temperatureConverter, benchmarkBuilder],
components: [
chronometer,
temperatureConverter,
benchmarkBuilder,
slaCalculator,
],
},
{
name: 'Text',

View file

@ -0,0 +1,12 @@
import { Clock } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'SLA calculator',
path: '/sla-calculator',
description: 'Service Level Agreement Calcultator',
keywords: ['sla', 'service', 'level', 'agreement', 'calculator'],
component: () => import('./sla-calculator.vue'),
icon: Clock,
createdAt: new Date('2024-05-11'),
});

View file

@ -0,0 +1,81 @@
import { describe, expect, it } from 'vitest';
import { downTimeToSLA, slaToDowntimes } from './sla-calculator.service';
describe('sla-calculator', () => {
describe('downTimeToSLA', () => {
it('compute correct values', () => {
expect(downTimeToSLA({
downTimeSeconds: 100,
mondayHours: 16,
tuesdayHours: 6,
wednesdayHours: 4,
thursdayHours: 24,
fridayHours: 21,
saturdayHours: 16,
sundayHours: 13,
})).to.deep.eq({
slaForDay: null,
slaForMonth: 99.99361155031703,
slaForQuarter: 99.99787051677234,
slaForWeek: 99.97222222222223,
slaForYear: 99.99946762919309,
});
expect(downTimeToSLA({
downTimeSeconds: 86400 / 10000,
})).to.deep.eq({
slaForDay: 99.99,
slaForMonth: 99.99967145115916,
slaForQuarter: 99.99989048371972,
slaForWeek: 99.99857142857142,
slaForYear: 99.99997262092992,
});
expect(downTimeToSLA({
downTimeSeconds: 86400 / 2,
})).to.deep.eq({
slaForDay: 50,
slaForMonth: 98.35725579580689,
slaForQuarter: 99.45241859860229,
slaForWeek: 92.85714285714286,
slaForYear: 99.86310464965058,
});
});
});
describe('slaToDowntimes', () => {
it('compute correct values', () => {
expect(slaToDowntimes({
targetSLA: 99,
mondayHours: 9,
tuesdayHours: 24,
wednesdayHours: 10,
thursdayHours: 8,
fridayHours: 3,
saturdayHours: 24,
sundayHours: 4,
})).to.deep.eq({
secondsPerDay: null,
secondsPerMonth: 12835.665,
secondsPerQuarter: 38506.995,
secondsPerWeek: 2952,
secondsPerYear: 154027.98,
});
expect(slaToDowntimes({
targetSLA: 99.99,
})).to.deep.eq({
secondsPerDay: 8.64,
secondsPerMonth: 262.9746,
secondsPerQuarter: 788.9238,
secondsPerWeek: 60.48,
secondsPerYear: 3155.6952,
});
expect(slaToDowntimes({
targetSLA: 99,
})).to.deep.eq({
secondsPerDay: 864,
secondsPerMonth: 26297.46,
secondsPerQuarter: 78892.38,
secondsPerWeek: 6048,
secondsPerYear: 315569.52,
});
});
});
});

View file

@ -0,0 +1,126 @@
import Big from 'big.js';
const hoursDay = Big('24');
const daysWeek = Big('7');
const monthsYear = Big('12');
const daysYear = Big('365.2425');
const weeksYear = daysYear.div(daysWeek);
const daysMonth = daysYear.div(monthsYear);
const weeksMonth = daysMonth.div(daysWeek);
interface DaysHours {
mondayHours?: number
tuesdayHours?: number
wednesdayHours?: number
thursdayHours?: number
fridayHours?: number
saturdayHours?: number
sundayHours?: number
}
function prepareDurations({
mondayHours = 24,
tuesdayHours = 24,
wednesdayHours = 24,
thursdayHours = 24,
fridayHours = 24,
saturdayHours = 24,
sundayHours = 24,
}: DaysHours) {
const durHoursWeek = Big(mondayHours).plus(tuesdayHours).plus(wednesdayHours).plus(thursdayHours).plus(fridayHours).plus(saturdayHours).plus(sundayHours);
const durSecondsWeek = durHoursWeek.mul('3600');
const durSecondsDay = durSecondsWeek.div(daysWeek);
const durSecondsMonth = durSecondsWeek.mul(weeksMonth);
const durSecondsQuarter = durSecondsMonth.mul('3');
const durSecondsYear = durSecondsWeek.mul(weeksYear);
const fullWeekHours = daysWeek.mul(hoursDay);
return {
durHoursWeek,
durSecondsWeek,
durSecondsDay,
durSecondsMonth,
durSecondsQuarter,
durSecondsYear,
fullWeekHours,
};
}
export function downTimeToSLA({
downTimeSeconds,
mondayHours = 24,
tuesdayHours = 24,
wednesdayHours = 24,
thursdayHours = 24,
fridayHours = 24,
saturdayHours = 24,
sundayHours = 24,
}: {
downTimeSeconds: number
} & DaysHours) {
const {
durHoursWeek,
durSecondsWeek,
durSecondsDay,
durSecondsMonth,
durSecondsQuarter,
durSecondsYear,
fullWeekHours,
} = prepareDurations({
mondayHours, tuesdayHours, wednesdayHours, thursdayHours, fridayHours, saturdayHours, sundayHours,
});
const one = Big('1');
const hundred = Big('100');
const downTimeSecondsBig = Big(downTimeSeconds);
return {
slaForDay: fullWeekHours.eq(durHoursWeek) ? one.minus(downTimeSecondsBig.div(durSecondsDay)).mul(hundred).toNumber() : null,
slaForWeek: one.minus(downTimeSecondsBig.div(durSecondsWeek)).mul(hundred).toNumber(),
slaForMonth: one.minus(downTimeSecondsBig.div(durSecondsMonth)).mul(hundred).toNumber(),
slaForQuarter: one.minus(downTimeSecondsBig.div(durSecondsQuarter)).mul(hundred).toNumber(),
slaForYear: one.minus(downTimeSecondsBig.div(durSecondsYear)).mul(hundred).toNumber(),
};
}
export function slaToDowntimes({
targetSLA,
mondayHours = 24,
tuesdayHours = 24,
wednesdayHours = 24,
thursdayHours = 24,
fridayHours = 24,
saturdayHours = 24,
sundayHours = 24,
}: {
targetSLA: number
mondayHours?: number
tuesdayHours?: number
wednesdayHours?: number
thursdayHours?: number
fridayHours?: number
saturdayHours?: number
sundayHours?: number
}) {
const {
durHoursWeek,
durSecondsWeek,
durSecondsDay,
durSecondsMonth,
durSecondsQuarter,
durSecondsYear,
fullWeekHours,
} = prepareDurations({
mondayHours, tuesdayHours, wednesdayHours, thursdayHours, fridayHours, saturdayHours, sundayHours,
});
const allowedDowntime = Big('1').minus(Big(targetSLA).div('100'));
return {
secondsPerDay: fullWeekHours.eq(durHoursWeek) ? allowedDowntime.mul(durSecondsDay).toNumber() : null,
secondsPerWeek: allowedDowntime.mul(durSecondsWeek).toNumber(),
secondsPerMonth: allowedDowntime.mul(durSecondsMonth).toNumber(),
secondsPerQuarter: allowedDowntime.mul(durSecondsQuarter).toNumber(),
secondsPerYear: allowedDowntime.mul(durSecondsYear).toNumber(),
};
}

View file

@ -0,0 +1,179 @@
<script setup lang="ts">
import prettyMilliseconds from 'pretty-ms';
import parse from 'parse-duration';
import { downTimeToSLA, slaToDowntimes } from './sla-calculator.service';
import { useQueryParam, useQueryParamOrStorage } from '@/composable/queryParams';
import { useValidation } from '@/composable/validation';
function prettySeconds(seconds: number | null) {
if (!seconds) {
return '';
}
return prettyMilliseconds(seconds * 1000);
}
const daysHours = useQueryParamOrStorage<{
mon?: number
tue?: number
wed?: number
thu?: number
fri?: number
sat?: number
sun?: number
}>({
name: 'days',
storageName: 'sla:days',
defaultValue: {
mon: 24,
tue: 24,
wed: 24,
thu: 24,
fri: 24,
sat: 24,
sun: 24,
},
});
const inputSLA = useQueryParam({ name: 'sla', defaultValue: 99.99 });
const outputDownTimes = computed(() => {
const weekdaysHours = daysHours.value;
const downtimesSeconds = slaToDowntimes({
targetSLA: inputSLA.value,
mondayHours: weekdaysHours.mon,
tuesdayHours: weekdaysHours.tue,
wednesdayHours: weekdaysHours.wed,
thursdayHours: weekdaysHours.thu,
fridayHours: weekdaysHours.fri,
saturdayHours: weekdaysHours.sat,
sundayHours: weekdaysHours.sun,
});
return {
durationPerDay: prettySeconds(downtimesSeconds.secondsPerDay),
durationPerMonth: prettySeconds(downtimesSeconds.secondsPerMonth),
durationPerQuarter: prettySeconds(downtimesSeconds.secondsPerQuarter),
durationPerWeek: prettySeconds(downtimesSeconds.secondsPerWeek),
durationPerYear: prettySeconds(downtimesSeconds.secondsPerYear),
};
});
const inputDuration = useQueryParam({ name: 'dur', defaultValue: '1s' });
const inputDurationValidation = useValidation({
source: inputDuration,
rules: [
{
message: 'Invalid duration',
validator: value => parse(value),
},
],
});
const inputDurationSeconds = computed(() => {
if (!inputDurationValidation.isValid) {
return 0;
}
return parse(inputDuration.value, 's') || 0;
});
const outputSLAs = computed(() => {
const weekdaysHours = daysHours.value;
const slas = downTimeToSLA({
downTimeSeconds: inputDurationSeconds.value,
mondayHours: weekdaysHours.mon,
tuesdayHours: weekdaysHours.tue,
wednesdayHours: weekdaysHours.wed,
thursdayHours: weekdaysHours.thu,
fridayHours: weekdaysHours.fri,
saturdayHours: weekdaysHours.sat,
sundayHours: weekdaysHours.sun,
});
return {
slaForDay: slas.slaForDay?.toPrecision(6),
slaForWeek: slas.slaForWeek.toPrecision(6),
slaForMonth: slas.slaForMonth.toPrecision(6),
slaForQuarter: slas.slaForQuarter.toPrecision(6),
slaForYear: slas.slaForYear.toPrecision(6),
};
});
</script>
<template>
<div>
<c-card title="SLA to downtimes" mb-2>
<n-form-item label="Agreed SLA" label-placement="left">
<n-input-number v-model:value="inputSLA" :min="0" :max="100" />
</n-form-item>
<n-divider />
<n-form-item v-if="outputDownTimes.durationPerDay" label="Daily downtime" label-placement="left" label-width="100">
<input-copyable :value="outputDownTimes.durationPerDay" />
</n-form-item>
<n-form-item label="Weekly downtime" label-placement="left" label-width="100">
<input-copyable :value="outputDownTimes.durationPerWeek" />
</n-form-item>
<n-form-item label="Monthly downtime" label-placement="left" label-width="100">
<input-copyable :value="outputDownTimes.durationPerMonth" />
</n-form-item>
<n-form-item label="Quarterly downtime" label-placement="left" label-width="100">
<input-copyable :value="outputDownTimes.durationPerQuarter" />
</n-form-item>
<n-form-item label="Yearly downtime" label-placement="left" label-width="100">
<input-copyable :value="outputDownTimes.durationPerYear" />
</n-form-item>
</c-card>
<c-card title="Downtime to SLA" mb-2>
<c-input-text
v-model:value="inputDuration"
label="Downtime duration"
label-position="left"
placeholder="Put downtime duration here..."
:validation="inputDurationValidation"
mb-2
/>
<n-divider />
<n-form-item v-if="outputSLAs.slaForDay" label="Daily SLA" label-placement="left" label-width="100">
<input-copyable :value="outputSLAs.slaForDay" />
</n-form-item>
<n-form-item label="Weekly SLA" label-placement="left" label-width="100">
<input-copyable :value="outputSLAs.slaForWeek" />
</n-form-item>
<n-form-item label="Monthly SLA" label-placement="left" label-width="100">
<input-copyable :value="outputSLAs.slaForMonth" />
</n-form-item>
<n-form-item label="Quarterly SLA" label-placement="left" label-width="100">
<input-copyable :value="outputSLAs.slaForQuarter" />
</n-form-item>
<n-form-item label="Yearly SLA" label-placement="left" label-width="100">
<input-copyable :value="outputSLAs.slaForYear" />
</n-form-item>
</c-card>
<c-card title="Weekdays hours">
<div flex flex-wrap gap-1>
<n-form-item label="Monday" flex-1>
<n-input-number v-model:value="daysHours.mon" :min="0" :max="24" />
</n-form-item>
<n-form-item label="Tuesday" flex-1>
<n-input-number v-model:value="daysHours.tue" :min="0" :max="24" />
</n-form-item>
<n-form-item label="Wednesday" flex-1>
<n-input-number v-model:value="daysHours.wed" :min="0" :max="24" />
</n-form-item>
<n-form-item label="Thursday" flex-1>
<n-input-number v-model:value="daysHours.thu" :min="0" :max="24" />
</n-form-item>
<n-form-item label="Friday" flex-1>
<n-input-number v-model:value="daysHours.fri" :min="0" :max="24" />
</n-form-item>
</div>
<div flex flex-wrap gap-1>
<n-form-item label="Saturday" flex-1>
<n-input-number v-model:value="daysHours.sat" :min="0" :max="24" />
</n-form-item>
<n-form-item label="Sunday" flex-1>
<n-input-number v-model:value="daysHours.sun" :min="0" :max="24" />
</n-form-item>
</div>
</c-card>
</div>
</template>