mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
Chart alerts: add schemas and read path (#95)
* Add schemas and read path for Dashboard chart alerts * Reuse existing `Alert` model * Show a bell icon on dashboard chart tile if it has an alert associated 
This commit is contained in:
parent
04f82d71db
commit
bbda6696bb
10 changed files with 131 additions and 16 deletions
6
.changeset/eight-cows-live.md
Normal file
6
.changeset/eight-cows-live.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
'@hyperdx/api': minor
|
||||
'@hyperdx/app': minor
|
||||
---
|
||||
|
||||
Chart alerts: add schemas and read path
|
||||
9
.vscode/settings.json
vendored
Normal file
9
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
|
|
@ -27,18 +27,30 @@ export type AlertChannel = {
|
|||
webhookId: string;
|
||||
};
|
||||
|
||||
export enum AlertSource {
|
||||
LOG = 'LOG',
|
||||
CHART = 'CHART',
|
||||
}
|
||||
|
||||
export interface IAlert {
|
||||
_id: ObjectId;
|
||||
channel: AlertChannel;
|
||||
cron: string;
|
||||
groupBy?: string;
|
||||
interval: AlertInterval;
|
||||
logView: ObjectId;
|
||||
message?: string;
|
||||
state: AlertState;
|
||||
threshold: number;
|
||||
timezone: string;
|
||||
type: AlertType;
|
||||
source?: AlertSource;
|
||||
|
||||
// Log alerts
|
||||
groupBy?: string;
|
||||
logView?: ObjectId;
|
||||
message?: string;
|
||||
|
||||
// Chart alerts
|
||||
dashboardId?: ObjectId;
|
||||
chartId?: string;
|
||||
}
|
||||
|
||||
const AlertSchema = new Schema<IAlert>(
|
||||
|
|
@ -47,10 +59,6 @@ const AlertSchema = new Schema<IAlert>(
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
threshold: {
|
||||
type: Number,
|
||||
required: true,
|
||||
|
|
@ -68,16 +76,43 @@ const AlertSchema = new Schema<IAlert>(
|
|||
required: true,
|
||||
},
|
||||
channel: Schema.Types.Mixed, // slack, email, etc
|
||||
logView: { type: mongoose.Schema.Types.ObjectId, ref: 'Alert' },
|
||||
state: {
|
||||
type: String,
|
||||
enum: AlertState,
|
||||
default: AlertState.OK,
|
||||
},
|
||||
source: {
|
||||
type: String,
|
||||
enum: AlertSource,
|
||||
required: false,
|
||||
default: AlertSource.LOG,
|
||||
},
|
||||
|
||||
// Log alerts
|
||||
logView: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Alert',
|
||||
required: false,
|
||||
},
|
||||
groupBy: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
|
||||
// Chart alerts
|
||||
dashboardId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Dashboard',
|
||||
required: false,
|
||||
},
|
||||
chartId: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
|
|
|||
|
|
@ -2,12 +2,29 @@ import mongoose, { Schema } from 'mongoose';
|
|||
|
||||
import type { ObjectId } from '.';
|
||||
|
||||
type Chart = {
|
||||
id: string;
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
series: {
|
||||
table: string;
|
||||
type: 'time' | 'histogram' | 'search' | 'number' | 'table' | 'markdown';
|
||||
aggFn: string;
|
||||
field?: string;
|
||||
where?: string;
|
||||
groupBy?: string[];
|
||||
}[];
|
||||
};
|
||||
|
||||
export interface IDashboard {
|
||||
_id: ObjectId;
|
||||
name: string;
|
||||
query: string;
|
||||
team: ObjectId;
|
||||
charts: any[]; // TODO: Type this eventually
|
||||
charts: Chart[];
|
||||
}
|
||||
|
||||
const DashboardSchema = new Schema<IDashboard>(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import Alert, {
|
|||
AlertChannel,
|
||||
AlertInterval,
|
||||
AlertType,
|
||||
AlertSource,
|
||||
} from '../../models/alert';
|
||||
import * as clickhouse from '../../clickhouse';
|
||||
import { SQLSerializer } from '../../clickhouse/searchQueryParser';
|
||||
|
|
@ -59,6 +60,7 @@ const createAlert = async ({
|
|||
cron: getCron(interval),
|
||||
groupBy,
|
||||
interval,
|
||||
source: AlertSource.LOG,
|
||||
logView: logViewId,
|
||||
threshold,
|
||||
timezone: 'UTC', // TODO: support different timezone
|
||||
|
|
@ -91,6 +93,7 @@ const updateAlert = async ({
|
|||
cron: getCron(interval),
|
||||
groupBy: groupBy ?? null,
|
||||
interval,
|
||||
source: AlertSource.LOG,
|
||||
logView: logViewId,
|
||||
threshold,
|
||||
timezone: 'UTC', // TODO: support different timezone
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import express from 'express';
|
||||
|
||||
import Dashboard from '../../models/dashboard';
|
||||
import Alert from '../../models/alert';
|
||||
import { isUserAuthenticated } from '../../middleware/auth';
|
||||
import { validateRequest } from 'zod-express-middleware';
|
||||
import { z } from 'zod';
|
||||
import { groupBy } from 'lodash';
|
||||
|
||||
// create routes that will get and update dashboards
|
||||
const router = express.Router();
|
||||
|
|
@ -51,8 +53,18 @@ router.get('/', isUserAuthenticated, async (req, res, next) => {
|
|||
{ _id: 1, name: 1, createdAt: 1, updatedAt: 1, charts: 1, query: 1 },
|
||||
).sort({ name: -1 });
|
||||
|
||||
const alertsByDashboard = groupBy(
|
||||
await Alert.find({
|
||||
dashboardId: { $in: dashboards.map(d => d._id) },
|
||||
}),
|
||||
'dashboardId',
|
||||
);
|
||||
|
||||
res.json({
|
||||
data: dashboards,
|
||||
data: dashboards.map(d => ({
|
||||
...d.toJSON(),
|
||||
alerts: alertsByDashboard[d._id.toString()],
|
||||
})),
|
||||
});
|
||||
} catch (e) {
|
||||
next(e);
|
||||
|
|
@ -147,6 +159,7 @@ router.delete('/:id', isUserAuthenticated, async (req, res, next) => {
|
|||
return res.sendStatus(400);
|
||||
}
|
||||
await Dashboard.findByIdAndDelete(dashboardId);
|
||||
await Alert.deleteMany({ dashboardId: dashboardId });
|
||||
res.json({});
|
||||
} catch (e) {
|
||||
next(e);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { serializeError } from 'serialize-error';
|
|||
import * as clickhouse from '../clickhouse';
|
||||
import * as config from '../config';
|
||||
import * as slack from '../utils/slack';
|
||||
import Alert, { AlertState, IAlert } from '../models/alert';
|
||||
import Alert, { AlertState, IAlert, AlertSource } from '../models/alert';
|
||||
import AlertHistory, { IAlertHistory } from '../models/alertHistory';
|
||||
import LogView from '../models/logView';
|
||||
import Webhook from '../models/webhook';
|
||||
|
|
@ -200,6 +200,14 @@ export const roundDownToXMinutes = (x: number) => roundDownTo(1000 * 60 * x);
|
|||
|
||||
const processAlert = async (now: Date, alert: IAlert) => {
|
||||
try {
|
||||
if (alert.source === AlertSource.CHART || !alert.logView) {
|
||||
logger.info({
|
||||
message: `[Not implemented] Skipping Chart alert processing`,
|
||||
alert,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const logView = await getLogViewEnhanced(alert.logView);
|
||||
|
||||
const previous: IAlertHistory | undefined = (
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import HDXHistogramChart from './HDXHistogramChart';
|
|||
import api from './api';
|
||||
import { LogTableWithSidePanel } from './LogTableWithSidePanel';
|
||||
import { parseTimeQuery, useNewTimeQuery, useTimeQuery } from './timeQuery';
|
||||
import type { Alert } from './types';
|
||||
import {
|
||||
EditSearchChartForm,
|
||||
EditMarkdownChartForm,
|
||||
|
|
@ -59,6 +60,7 @@ type Dashboard = {
|
|||
id: string;
|
||||
name: string;
|
||||
charts: Chart[];
|
||||
alerts?: Alert[];
|
||||
query?: string;
|
||||
};
|
||||
|
||||
|
|
@ -85,6 +87,7 @@ const Tile = forwardRef(
|
|||
queued,
|
||||
onSettled,
|
||||
granularity,
|
||||
hasAlert,
|
||||
|
||||
// Properties forwarded by grid layout
|
||||
className,
|
||||
|
|
@ -102,6 +105,7 @@ const Tile = forwardRef(
|
|||
onSettled?: () => void;
|
||||
queued?: boolean;
|
||||
granularity: Granularity | undefined;
|
||||
hasAlert?: boolean;
|
||||
|
||||
// Properties forwarded by grid layout
|
||||
className?: string;
|
||||
|
|
@ -190,10 +194,18 @@ const Tile = forwardRef(
|
|||
<div className="d-flex justify-content-between align-items-center mb-3 cursor-grab">
|
||||
<div className="fs-7 text-muted">{chart.name}</div>
|
||||
<i className="bi bi-grip-horizontal text-muted" />
|
||||
<div className="fs-7 text-muted cursor-pointer">
|
||||
<div className="fs-7 text-muted d-flex gap-2 align-items-center">
|
||||
{hasAlert && (
|
||||
<div
|
||||
className="rounded px-1 text-muted bg-grey opacity-90 cursor-default"
|
||||
title="Has alert"
|
||||
>
|
||||
<span className="bi bi-bell" />
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="link"
|
||||
className="text-muted-hover p-0 me-2"
|
||||
className="text-muted-hover p-0"
|
||||
size="sm"
|
||||
onClick={onEditClick}
|
||||
>
|
||||
|
|
@ -673,6 +685,7 @@ export default function DashboardPage() {
|
|||
dateRange={searchedTimeRange}
|
||||
onEditClick={() => setEditedChart(chart)}
|
||||
granularity={granularityQuery}
|
||||
hasAlert={dashboard?.alerts?.some(a => a.chartId === chart.id)}
|
||||
onDeleteClick={() => {
|
||||
if (dashboard != null) {
|
||||
setDashboard({
|
||||
|
|
|
|||
|
|
@ -67,14 +67,21 @@ export type Alert = {
|
|||
_id: string;
|
||||
channel: AlertChannel;
|
||||
cron: string;
|
||||
groupBy?: string;
|
||||
interval: AlertInterval;
|
||||
logView: string;
|
||||
message?: string;
|
||||
state: 'ALERT' | 'OK';
|
||||
threshold: number;
|
||||
timezone: string;
|
||||
type: 'presence' | 'absence';
|
||||
source: 'LOG' | 'CHART';
|
||||
|
||||
// Log alerts
|
||||
logView?: string;
|
||||
message?: string;
|
||||
groupBy?: string;
|
||||
|
||||
// Chart alerts
|
||||
dashboardId?: string;
|
||||
chartId?: string;
|
||||
};
|
||||
|
||||
export type Session = {
|
||||
|
|
|
|||
|
|
@ -143,6 +143,10 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
.cursor-default {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue