Skip to content

Commit

Permalink
feat: Add traceroute (#245)
Browse files Browse the repository at this point in the history
  • Loading branch information
rdubrock authored Sep 8, 2021
1 parent 7d3b4ca commit 89ab9b1
Show file tree
Hide file tree
Showing 27 changed files with 1,809 additions and 90 deletions.
4 changes: 2 additions & 2 deletions src/components/AlertRuleForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ export const AlertRuleForm = ({ rule, onSubmit }: Props) => {
<span className={styles.inlineText}>will fire an alert if less than </span>
<Field
invalid={Boolean(errors?.probePercentage)}
error={errors?.probePercentage?.message}
error={errors?.probePercentage?.message?.toString()}
className={styles.noMargin}
>
<Input
Expand All @@ -238,7 +238,7 @@ export const AlertRuleForm = ({ rule, onSubmit }: Props) => {
<span className={styles.inlineText}>% of probes report connection success for</span>
<Field
invalid={Boolean(errors?.timeCount)}
error={errors?.timeCount?.message}
error={errors?.timeCount?.message?.toString()}
className={styles.noMargin}
>
<Input
Expand Down
13 changes: 9 additions & 4 deletions src/components/CheckEditor/CheckEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import { CheckEditor } from './CheckEditor';
import { getInstanceMock } from '../../datasource/__mocks__/DataSource';
import userEvent from '@testing-library/user-event';
import { InstanceContext } from 'contexts/InstanceContext';
import { AppPluginMeta, DataSourceSettings } from '@grafana/data';
import { AppPluginMeta, DataSourceSettings, FeatureToggles } from '@grafana/data';
import { DNS_RESPONSE_MATCH_OPTIONS } from 'components/constants';
import { FeatureFlagProvider } from 'components/FeatureFlagProvider';
jest.setTimeout(60000);

// Mock useAlerts hook
Expand Down Expand Up @@ -147,10 +148,14 @@ const renderCheckEditor = async ({ check = defaultCheck, withAlerting = true } =
alertRuler: withAlerting ? ({} as DataSourceSettings) : undefined,
};
const meta = {} as AppPluginMeta<GlobalSettings>;
const featureToggles = ({ traceroute: true } as unknown) as FeatureToggles;
const isFeatureEnabled = jest.fn(() => true);
render(
<InstanceContext.Provider value={{ instance, loading: false, meta }}>
<CheckEditor check={check} onReturn={onReturn} />
</InstanceContext.Provider>
<FeatureFlagProvider overrides={{ featureToggles, isFeatureEnabled }}>
<InstanceContext.Provider value={{ instance, loading: false, meta }}>
<CheckEditor check={check} onReturn={onReturn} />
</InstanceContext.Provider>
</FeatureFlagProvider>
);
await waitFor(() => expect(screen.getByText('Check Details')).toBeInTheDocument());
return instance;
Expand Down
22 changes: 17 additions & 5 deletions src/components/CheckEditor/CheckEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useState, useMemo, useContext } from 'react';
import { css } from '@emotion/css';
import { Button, ConfirmModal, Field, Input, HorizontalGroup, Select, Legend, Alert, useStyles } from '@grafana/ui';
import { useAsyncCallback } from 'react-async-hook';
import { Check, CheckType, OrgRole, CheckFormValues, SubmissionError } from 'types';
import { Check, CheckType, OrgRole, CheckFormValues, SubmissionErrorWrapper, FeatureName } from 'types';
import { hasRole } from 'utils';
import { getDefaultValuesFromCheck, getCheckFromFormValues } from './checkFormTransformations';
import { validateJob, validateTarget } from 'validation';
Expand All @@ -17,6 +17,7 @@ import { GrafanaTheme } from '@grafana/data';
import { CheckUsage } from '../CheckUsage';
import { CheckFormAlert } from 'components/CheckFormAlert';
import { InstanceContext } from 'contexts/InstanceContext';
import { useFeatureFlag } from 'hooks/useFeatureFlag';
import { trackEvent, trackException } from 'analytics';

interface Props {
Expand Down Expand Up @@ -52,6 +53,8 @@ export const CheckEditor = ({ check, onReturn }: Props) => {
const [showDeleteModal, setShowDeleteModal] = useState(false);
const styles = useStyles(getStyles);
const defaultValues = useMemo(() => getDefaultValuesFromCheck(check), [check]);
const { isEnabled: tracerouteEnabled } = useFeatureFlag(FeatureName.Traceroute);

const formMethods = useForm<CheckFormValues>({ defaultValues, mode: 'onChange' });
const selectedCheckType = formMethods.watch('checkType').value ?? CheckType.PING;

Expand All @@ -73,7 +76,7 @@ export const CheckEditor = ({ check, onReturn }: Props) => {
onReturn(true);
});

const submissionError = error as SubmissionError;
const submissionError = (error as unknown) as SubmissionErrorWrapper;
if (error) {
trackException(`addNewCheckSubmitException: ${error}`);
}
Expand Down Expand Up @@ -101,7 +104,11 @@ export const CheckEditor = ({ check, onReturn }: Props) => {
<Select
{...field}
placeholder="Check type"
options={CHECK_TYPE_OPTIONS}
options={
tracerouteEnabled
? CHECK_TYPE_OPTIONS
: CHECK_TYPE_OPTIONS.filter(({ value }) => value !== CheckType.Traceroute)
}
width={30}
disabled={check?.id ? true : false}
/>
Expand Down Expand Up @@ -138,7 +145,12 @@ export const CheckEditor = ({ check, onReturn }: Props) => {
control={formMethods.control}
rules={{
required: true,
validate: (target) => validateTarget(selectedCheckType, target),
validate: (target) => {
// We have to get refetch the check type value from form state in the validation because the value will be stale if we rely on the the .watch method in the render
const targetFormValue = formMethods.getValues().checkType;
const selectedCheckType = targetFormValue.value as CheckType;
return validateTarget(selectedCheckType, target);
},
}}
render={({ field }) => (
<CheckTarget
Expand Down Expand Up @@ -189,7 +201,7 @@ export const CheckEditor = ({ check, onReturn }: Props) => {
{submissionError && (
<div className={styles.submissionError}>
<Alert title="Save failed" severity="error">
{`${submissionError.status}: ${submissionError.message ?? submissionError.msg ?? 'Something went wrong'}`}
{`${submissionError.status}: ${submissionError.data?.msg ?? 'Something went wrong'}`}
</Alert>
</div>
)}
Expand Down
4 changes: 4 additions & 0 deletions src/components/CheckEditor/CheckSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { PingSettingsForm } from 'components/PingSettings';
import { HttpSettingsForm } from 'components/http/HttpSettings';
import DnsSettingsForm from 'components/DnsSettings';
import { TcpSettingsForm } from 'components/TcpSettings';
import { TracerouteSettingsForm } from 'components/TracerouteSettingsForm';

interface Props {
isEditor: boolean;
Expand All @@ -24,5 +25,8 @@ export const CheckSettings: FC<Props> = ({ isEditor, typeOfCheck }) => {
case CheckType.TCP: {
return <TcpSettingsForm isEditor={isEditor} />;
}
case CheckType.Traceroute: {
return <TracerouteSettingsForm isEditor={isEditor} />;
}
}
};
59 changes: 36 additions & 23 deletions src/components/CheckEditor/ProbeOptions.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { useState, useEffect, useContext } from 'react';
import { Field } from '@grafana/ui';
import { Field, Input } from '@grafana/ui';
import CheckProbes from './CheckProbes';
import { InstanceContext } from 'contexts/InstanceContext';
import { Probe } from 'types';
import { Probe, CheckType } from 'types';
import { SliderInput } from 'components/SliderInput';
import { Subheader } from 'components/Subheader';
import { useFormContext, Controller } from 'react-hook-form';
Expand All @@ -19,10 +19,13 @@ export const ProbeOptions = ({ frequency, timeout, isEditor, probes }: Props) =>
const [availableProbes, setAvailableProbes] = useState<Probe[]>([]);
const {
control,
watch,
formState: { errors },
} = useFormContext();
const { instance } = useContext(InstanceContext);

const checkType = watch('checkType').value;

useEffect(() => {
const abortController = new AbortController();
const fetchProbes = async () => {
Expand Down Expand Up @@ -58,37 +61,47 @@ export const ProbeOptions = ({ frequency, timeout, isEditor, probes }: Props) =>
<Field
label="Frequency"
description="How frequently the check should run."
disabled={!isEditor}
disabled={!isEditor || checkType === CheckType.Traceroute}
invalid={Boolean(errors.frequency)}
error={errors.frequency?.message}
>
<SliderInput
validate={validateFrequency}
name="frequency"
prefixLabel={'Every'}
suffixLabel={'seconds'}
min={10.0}
max={120.0}
defaultValue={frequency / 1000}
/>
{checkType === CheckType.Traceroute ? (
// This is just a placeholder for now, the frequency for traceroute checks is hardcoded in the submit
<Input value={120} prefix="Every" suffix="seconds" width={20} />
) : (
<SliderInput
validate={(value) => validateFrequency(value, checkType)}
name="frequency"
prefixLabel={'Every'}
suffixLabel={'seconds'}
min={checkType === CheckType.Traceroute ? 60.0 : 10.0}
max={checkType === CheckType.Traceroute ? 240.0 : 120.0}
defaultValue={checkType === CheckType.Traceroute ? 120 : frequency / 1000}
/>
)}
</Field>
<Field
label="Timeout"
description="Maximum execution time for a check"
disabled={!isEditor}
disabled={!isEditor || checkType === CheckType.Traceroute}
invalid={Boolean(errors.timeout)}
error={errors.timeout?.message}
>
<SliderInput
name="timeout"
validate={validateTimeout}
defaultValue={timeout / 1000}
max={10.0}
min={1.0}
step={0.5}
suffixLabel="seconds"
prefixLabel="After"
/>
{checkType === CheckType.Traceroute ? (
// This is just a placeholder for now, the timeout for traceroute checks is hardcoded in the submit
<Input value={30} prefix="Every" suffix="seconds" width={20} />
) : (
<SliderInput
name="timeout"
validate={(value) => validateTimeout(value, checkType)}
defaultValue={checkType === CheckType.Traceroute ? 30 : timeout / 1000}
max={checkType === CheckType.Traceroute ? 30.0 : 10.0}
min={1.0}
step={0.5}
suffixLabel="seconds"
prefixLabel="After"
/>
)}
</Field>
</div>
);
Expand Down
54 changes: 52 additions & 2 deletions src/components/CheckEditor/checkFormTransformations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
AlertSensitivity,
TCPQueryResponse,
TLSConfig,
TracerouteSettings,
TracerouteSettingsFormValues,
} from 'types';

import {
Expand Down Expand Up @@ -243,6 +245,17 @@ const getDnsSettingsFormValues = (settings: Settings): DnsSettingsFormValues =>
};
};

const getTracerouteSettingsFormValues = (settings: Settings): TracerouteSettingsFormValues => {
const tracerouteSettings = settings.traceroute ?? (fallbackSettings(CheckType.Traceroute) as TracerouteSettings);

return {
firstHop: String(tracerouteSettings.firstHop ?? 1),
maxHops: String(tracerouteSettings.maxHops),
retries: String(tracerouteSettings.retries ?? 0),
maxUnknownHops: String(tracerouteSettings.maxUnknownHops),
};
};

const getFormSettingsForCheck = (settings: Settings): SettingsFormValues => {
const type = checkType(settings);
switch (type) {
Expand All @@ -252,6 +265,8 @@ const getFormSettingsForCheck = (settings: Settings): SettingsFormValues => {
return { tcp: getTcpSettingsFormValues(settings) };
case CheckType.DNS:
return { dns: getDnsSettingsFormValues(settings) };
case CheckType.Traceroute:
return { traceroute: getTracerouteSettingsFormValues(settings) };
case CheckType.PING:
default:
return { ping: getPingSettingsFormValues(settings) };
Expand All @@ -264,6 +279,7 @@ const getAllFormSettingsForCheck = (): SettingsFormValues => {
tcp: getTcpSettingsFormValues(fallbackSettings(CheckType.TCP)),
dns: getDnsSettingsFormValues(fallbackSettings(CheckType.DNS)),
ping: getPingSettingsFormValues(fallbackSettings(CheckType.PING)),
traceroute: getTracerouteSettingsFormValues(fallbackSettings(CheckType.Traceroute)),
};
};

Expand Down Expand Up @@ -530,6 +546,20 @@ const getDnsSettings = (
};
};

const getTracerouteSettings = (
settings: TracerouteSettingsFormValues | undefined,
defaultSettings: TracerouteSettingsFormValues | undefined
): TracerouteSettings => {
const fallbackValues = fallbackSettings(CheckType.Traceroute).traceroute as TracerouteSettings;
const updatedSettings = settings ?? defaultSettings ?? fallbackValues;
return {
firstHop: parseInt(String(updatedSettings.firstHop), 10),
maxHops: parseInt(String(updatedSettings.maxHops), 10),
retries: 0,
maxUnknownHops: parseInt(String(updatedSettings.maxUnknownHops), 10),
};
};

const getSettingsFromFormValues = (formValues: Partial<CheckFormValues>, defaultValues: CheckFormValues): Settings => {
const checkType = getValueFromSelectable(formValues.checkType ?? defaultValues.checkType);
switch (checkType) {
Expand All @@ -541,11 +571,31 @@ const getSettingsFromFormValues = (formValues: Partial<CheckFormValues>, default
return { dns: getDnsSettings(formValues.settings?.dns, defaultValues.settings.dns) };
case CheckType.PING:
return { ping: getPingSettings(formValues.settings?.ping, defaultValues.settings.ping) };
case CheckType.Traceroute:
return {
traceroute: {
...getTracerouteSettings(formValues.settings?.traceroute, defaultValues.settings.traceroute),
},
};
default:
throw new Error(`Check type of ${checkType} is invalid`);
}
};

const getTimeoutFromFormValue = (timeout: number, checkType?: CheckType): number => {
if (checkType === CheckType.Traceroute) {
return 30000;
}
return timeout * 1000;
};

const getFrequencyFromFormValue = (frequency: number, checkType?: CheckType): number => {
if (checkType === CheckType.Traceroute) {
return 120000;
}
return frequency * 1000;
};

export const getCheckFromFormValues = (
formValues: Omit<CheckFormValues, 'alert'>,
defaultValues: CheckFormValues
Expand All @@ -556,8 +606,8 @@ export const getCheckFromFormValues = (
enabled: formValues.enabled,
labels: formValues.labels ?? [],
probes: formValues.probes,
timeout: formValues.timeout * 1000,
frequency: formValues.frequency * 1000,
timeout: getTimeoutFromFormValue(formValues.timeout, getValueFromSelectable(formValues.checkType)),
frequency: getFrequencyFromFormValue(formValues.frequency, getValueFromSelectable(formValues.checkType)),
alertSensitivity: getValueFromSelectable(formValues.alertSensitivity) ?? AlertSensitivity.None,
settings: getSettingsFromFormValues(formValues, defaultValues),
basicMetricsOnly: !formValues.publishAdvancedMetrics,
Expand Down
7 changes: 7 additions & 0 deletions src/components/CheckTarget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ const getTargetHelpText = (typeOfCheck: CheckType | undefined): TargetHelpInfo =
};
break;
}
case CheckType.Traceroute: {
resp = {
text: 'Hostname to send traceroute',
example: 'grafana.com',
};
break;
}
}
return resp;
};
Expand Down
Loading

0 comments on commit 89ab9b1

Please sign in to comment.