Skip to content

Commit

Permalink
feat: support grafana alerts
Browse files Browse the repository at this point in the history
  • Loading branch information
alvarolorentedev committed Aug 1, 2024
1 parent 36d72a8 commit 681ac60
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 4 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
"tsc:check": "tsc --noEmit --skipLibCheck",
"lint:check": "eslint src --ext .ts,.tsx --max-warnings=0",
"lint:fix": "eslint src --ext .ts,.tsx --fix",
"format:fix": "npm run prettier --write src",
"format:check": "npm run prettier --check src",
"format:fix": "prettier --write src",
"format:check": "prettier --check src",
"chocolatey:prepare": "node build/chocolatey/script.js",
"lint-staged": "lint-staged",
"prepare": "husky install",
Expand Down
2 changes: 2 additions & 0 deletions src/main/observers/ObserverManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { DatadogMonitor } from './DatadogMonitor';
import { MapType } from '../../types/MapType';
import { Sentry } from './Sentry';
import { NewRelic } from './NewRelic';
import { Grafana } from './grafana';

export class ObserverManager {
private observers: Observer[];
Expand All @@ -22,6 +23,7 @@ export class ObserverManager {
datadogMonitor: (configuration: any) => new DatadogMonitor(configuration as any),
sentry: (configuration: any) => new Sentry(configuration as any),
newRelic: (configuration: any) => new NewRelic(configuration as any),
grafana: (configuration: any) => new Grafana(configuration as any),
};

constructor(
Expand Down
112 changes: 112 additions & 0 deletions src/main/observers/grafana/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { faker } from '@faker-js/faker';
import { Status } from '../../../types/Status';
import { GrafanaConfiguration } from '../../../types/GrafanaConfiguration';
import { Grafana } from './index';

const fetchtMock = jest.fn();
jest.mock('electron-fetch', () => {
return {
__esModule: true,
default: (...all: any) => fetchtMock(...all),
};
});

describe('NewRelic', () => {
describe('getState', () => {
let config: GrafanaConfiguration;
let observer: Grafana;

let expectedUrl: string;
let expectedSite: string;

beforeEach(() => {
fetchtMock.mockClear();
config = {
type: 'grafana',
url: faker.internet.url(),
authToken: faker.lorem.word(),
alias: faker.lorem.word(),
};
expectedUrl = `${config.url}/api/v1/provisioning/alert-rules`;
expectedSite = `${config.url}/alerting`;
observer = new Grafana(config);
});

it('shoulds return NA status if request return diferent value than 200', async () => {
fetchtMock.mockResolvedValue({
json: () => Promise.resolve('kaboom'),
ok: false,
});
const result = await observer.getState();
expect(fetchtMock).toBeCalledWith(expectedUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${config.authToken}`,
},
});
expect(result).toEqual({
name: config.alias,
status: Status.NA,
link: expectedSite,
});
});
it('shoulds return SUCCESS status if request return empty violations array', async () => {
fetchtMock.mockResolvedValue({
json: () =>
Promise.resolve([
{
execErrState: 'Success',
},
{
execErrState: 'Success',
},
{
execErrState: 'Success',
},
]),
ok: true,
});
const result = await observer.getState();
expect(fetchtMock).toBeCalledWith(expectedUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${config.authToken}`,
},
});
expect(result).toEqual({
name: config.alias,
status: Status.SUCCESS,
link: expectedSite,
});
});
it('shoulds return FAILURE status if request have active alarms', async () => {
fetchtMock.mockResolvedValue({
json: () =>
Promise.resolve([
{
execErrState: 'Error',
},
{
execErrState: 'Success',
},
{
execErrState: 'Success',
},
]),
ok: true,
});
const result = await observer.getState();
expect(fetchtMock).toBeCalledWith(expectedUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${config.authToken}`,
},
});
expect(result).toEqual({
name: config.alias,
status: Status.FAILURE,
link: expectedSite,
});
});
});
});
46 changes: 46 additions & 0 deletions src/main/observers/grafana/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { State } from '../../../types/State';
import { Observer } from '../../../types/Observer';
import { Status } from '../../../types/Status';
import { GrafanaConfiguration } from '../../../types/GrafanaConfiguration';
import fetch from 'electron-fetch';

export class Grafana implements Observer {
private readonly url: string;
private readonly site: string;
private readonly alias: string;
private readonly authToken: string;

constructor({ url, alias, authToken }: GrafanaConfiguration) {
this.url = `${url}/api/v1/provisioning/alert-rules`;
this.site = `${url}/alerting`;
this.alias = alias || `Grafana: ${url}`;
this.authToken = authToken;
}
public async getState(): Promise<State> {
try {
const response = await fetch(this.url, {
method: 'GET',
headers: {
Authorization: `Bearer ${this.authToken}`,
},
});
if (!response.ok) throw new Error('response is invalid');
const alerRules = await response.json();
return {
name: this.alias,
status: this.getStatus(alerRules),
link: this.site,
};
} catch (_) {
return {
name: this.alias,
status: Status.NA,
link: this.site,
};
}
}

private getStatus(alertRules: any[]): Status {
return alertRules.some((alertRule: any) => alertRule.execErrState === 'Error') ? Status.FAILURE : Status.SUCCESS;
}
}
52 changes: 52 additions & 0 deletions src/renderer/components/Grafana/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* @jest-environment jsdom
*/
import React from 'react';
import { render, fireEvent, waitFor, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { Grafana } from './';
import { faker } from '@faker-js/faker';

jest.mock('@mui/material/TextField', () => ({
__esModule: true,
default: (props: any) => <input data-testid={`textField-${props.label}`} {...props} />,
}));

describe('Grafana Monitor', () => {
const expectedObservable = {
apiKey: faker.lorem.word(),
};
const expectedIndex = faker.random.numeric();
const updateFieldMock = jest.fn();
const translateMock = (val: string): string => val;
beforeEach(() => {
updateFieldMock.mockClear();
render(
<Grafana
observable={expectedObservable}
index={expectedIndex}
updateFieldWithValue={updateFieldMock}
translate={translateMock}
/>
);
});
describe.each([
['URL', 'url', undefined],
['Authorization Token', 'authToken', 'password'],
])('%s', (label: string, value: string, type: string) => {
it('should have correct textfield attributes', () => {
const textfield = screen.getByTestId(`textField-${label}`);
expect(textfield).toHaveAttribute('label', label);
expect(textfield).toHaveAttribute('variant', 'outlined');
if (type) expect(textfield).toHaveAttribute('type', type);
expect(textfield).toHaveAttribute('value', (expectedObservable as any)[value]);
});

it('should call update field on change event', () => {
const expectedValue = faker.lorem.word();
const textfield = screen.getByTestId(`textField-${label}`);
fireEvent.change(textfield, { target: { value: expectedValue } });
expect(updateFieldMock).toBeCalledWith(value, expectedIndex, expectedValue);
});
});
});
22 changes: 22 additions & 0 deletions src/renderer/components/Grafana/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import TextField from '@mui/material/TextField';

export const Grafana = ({ observable, index, updateFieldWithValue, translate }: any) => (
<>
<TextField
label={translate('URL')}
variant="outlined"
value={observable.url}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => updateFieldWithValue('url', index, event.target.value)}
/>
<TextField
label={translate('Authorization Token')}
variant="outlined"
type="password"
value={observable.authToken}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
updateFieldWithValue('authToken', index, event.target.value)
}
/>
</>
);
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CCTray } from '../../CCTray';
import { DatadogMonitor } from '../../DatadogMonitor';
import { Sentry } from '../../Sentry';
import { NewRelic } from '../../NewRelic';
import { Grafana } from '../../Grafana';

export const observersComponentBuilderMap: MapType<
(observable: any, index: number, updateFieldWithValue: any, translate: any) => JSX.Element
Expand Down Expand Up @@ -34,4 +35,7 @@ export const observersComponentBuilderMap: MapType<
newRelic: (observable: any, index: number, updateFieldWithValue: any, translate: any) => (
<NewRelic observable={observable} index={index} updateFieldWithValue={updateFieldWithValue} translate={translate} />
),
grafana: (observable: any, index: number, updateFieldWithValue: any, translate: any) => (
<Grafana observable={observable} index={index} updateFieldWithValue={updateFieldWithValue} translate={translate} />
),
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export const observersTitleBuilderMap: MapType<(observable: any) => string> = {
datadogMonitor: (observable: any) => `Datadog: ${observable.alias || `${observable.site}/${observable.monitorId}`}`,
sentry: (observable: any) => `Sentry: ${observable.alias || `${observable.organization}/${observable.project}`}`,
newRelic: (observable: any) => `NewRelic: ${observable.alias || `alerts`}`,
grafana: (observable: any) => `Grafana: ${observable.alias || observable.url}`,
};
6 changes: 4 additions & 2 deletions src/renderer/components/Observers/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,13 @@ describe('dropzone', () => {
const select = within(detailsStack).getByTestId('select');
expect(select).toHaveAttribute('label', 'Observer Type');
const menuItems = within(select).getAllByTestId('menu-item');
expect(menuItems).toHaveLength(5);
expect(menuItems).toHaveLength(6);
expect(menuItems[0]).toHaveAttribute('value', 'githubAction');
expect(menuItems[1]).toHaveAttribute('value', 'ccTray');
expect(menuItems[2]).toHaveAttribute('value', 'datadogMonitor');
expect(menuItems[3]).toHaveAttribute('value', 'sentry');
expect(menuItems[4]).toHaveAttribute('value', 'newRelic');
expect(menuItems[5]).toHaveAttribute('value', 'grafana');
expect(within(detailsStack).getByTestId('githubAction')).toBeInTheDocument();
const alias = within(detailsStack).getByTestId('text-field');
expect(alias).toHaveAttribute('label', 'Alias');
Expand Down Expand Up @@ -232,12 +233,13 @@ describe('dropzone', () => {
const select = within(detailsStack).getByTestId('select');
expect(select).toHaveAttribute('label', 'Observer Type');
const menuItems = within(select).getAllByTestId('menu-item');
expect(menuItems).toHaveLength(5);
expect(menuItems).toHaveLength(6);
expect(menuItems[0]).toHaveAttribute('value', 'githubAction');
expect(menuItems[1]).toHaveAttribute('value', 'ccTray');
expect(menuItems[2]).toHaveAttribute('value', 'datadogMonitor');
expect(menuItems[3]).toHaveAttribute('value', 'sentry');
expect(menuItems[4]).toHaveAttribute('value', 'newRelic');
expect(menuItems[5]).toHaveAttribute('value', 'grafana');
const alias = within(detailsStack).getByTestId('text-field');
expect(alias).toHaveAttribute('label', 'Alias');
expect(alias).toHaveAttribute('variant', 'outlined');
Expand Down
1 change: 1 addition & 0 deletions src/renderer/components/Observers/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const Observers = ({ observables, add, remove, update, save, translate }:
<MenuItem value={'datadogMonitor'}>Datadog Monitor</MenuItem>
<MenuItem value={'sentry'}>Sentry</MenuItem>
<MenuItem value={'newRelic'}>New Relic</MenuItem>
<MenuItem value={'grafana'}>Grafana</MenuItem>
</Select>
{getComponent(observable, index, update)}
<TextField
Expand Down
6 changes: 6 additions & 0 deletions src/types/GrafanaConfiguration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ObserverConfiguration } from './ObserverConfiguration';

export type GrafanaConfiguration = ObserverConfiguration & {
url: string;
authToken: string;
};

0 comments on commit 681ac60

Please sign in to comment.