Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion alert/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ func (opts renderData) Normalize() (*renderData, error) {

err := validate.Many(
validate.Search("Search", opts.Search),
validate.Range("Limit", opts.Limit, 0, search.MaxResults),
validate.Range("Limit", opts.Limit, 0, 1001),
validate.Range("Status", len(opts.Status), 0, 3),
validate.ManyUUID("Services", opts.ServiceFilter.IDs, 50),
validate.Range("Omit", len(opts.Omit), 0, 50),
Expand Down
2 changes: 1 addition & 1 deletion graphql2/graphqlapp/alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ func (q *Query) Alerts(ctx context.Context, opts *graphql2.AlertSearchOptions) (

err = validate.Many(
validate.Range("ServiceIDs", len(opts.FilterByServiceID), 0, 50),
validate.Range("First", s.Limit, 1, 100),
validate.Range("First", s.Limit, 1, 1000),
)
if err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@
"prettier": "2.7.1",
"stylelint": "14.9.0",
"stylelint-config-standard": "26.0.0",
"typescript": "4.7.2"
"typescript": "4.7.4"
}
}
255 changes: 40 additions & 215 deletions web/src/app/services/AlertMetrics/AlertMetrics.tsx
Original file line number Diff line number Diff line change
@@ -1,257 +1,82 @@
import React, {
useMemo,
useState,
useEffect,
useRef,
useDeferredValue,
} from 'react'
import React, { useMemo } from 'react'
import { Card, CardContent, CardHeader, Grid } from '@mui/material'
import { gql, useClient } from 'urql'
import { DateTime, Duration, Interval } from 'luxon'
import { useURLParams } from '../../actions/hooks'
import AlertMetricsFilter, {
DATE_FORMAT,
MAX_DAY_COUNT,
} from './AlertMetricsFilter'
import { DateTime, DateTimeUnit, Duration, Interval } from 'luxon'
import { useURLParam } from '../../actions/hooks'
import AlertMetricsFilter from './AlertMetricsFilter'
import AlertCountGraph from './AlertCountGraph'
import AlertMetricsTable from './AlertMetricsTable'
import AlertAveragesGraph from './AlertAveragesGraph'
import { Alert } from '../../../schema'
import { GenericError } from '../../error-pages'
import _ from 'lodash'

const alertsQuery = gql`
query alerts($input: AlertSearchOptions!) {
alerts(input: $input) {
nodes {
id
alertID
summary
status
service {
name
id
}
createdAt
metrics {
closedAt
timeToClose
timeToAck
escalated
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`

const QUERY_LIMIT = 100
import { GenericError, ObjectNotFound } from '../../error-pages'
import { useWorker } from '../../worker'
import { AlertMetricsOpts } from './useAlertMetrics'
import { useAlerts } from './useAlerts'
import { useQuery } from 'urql'
import Spinner from '../../loading/components/Spinner'

export type AlertMetricsProps = {
serviceID: string
}

type AlertsData = {
alerts: Alert[]
loading: boolean
error: Error | undefined
}

function useAlerts(
serviceID: string,
since: string,
until: string,
isValidRange: boolean,
): AlertsData {
const depKey = `${serviceID}-${since}-${until}`
const [alerts, setAlerts] = useState<Alert[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | undefined>()
const key = useRef(depKey)
key.current = depKey
const renderAlerts = useDeferredValue(alerts)

useEffect(() => {
return () => {
// cancel on unmount
key.current = ''
}
}, [])

const client = useClient()
const fetch = React.useCallback(async () => {
setAlerts([])
setLoading(true)
setError(undefined)
if (!isValidRange) {
return
}
async function fetchAlerts(
cursor: string,
): Promise<[Alert[], boolean, string, Error | undefined]> {
const q = await client
.query(alertsQuery, {
input: {
filterByServiceID: [serviceID],
first: QUERY_LIMIT,
notClosedBefore: since,
closedBefore: until,
filterByStatus: ['StatusClosed'],
after: cursor,
},
})
.toPromise()

if (q.error) {
return [[], false, '', q.error]
}

return [
q.data.alerts.nodes,
q.data.alerts.pageInfo.hasNextPage,
q.data.alerts.pageInfo.endCursor,
undefined,
]
}

const throttledSetAlerts = _.throttle(setAlerts, 1000)

let [alerts, hasNextPage, endCursor, error] = await fetchAlerts('')
if (key.current !== depKey) return // abort if the key has changed
if (error) {
setError(error)
throttledSetAlerts.cancel()
return
}
let allAlerts = alerts
setAlerts(allAlerts)
while (hasNextPage) {
;[alerts, hasNextPage, endCursor, error] = await fetchAlerts(endCursor)
if (key.current !== depKey) return // abort if the key has changed
if (error) {
setError(error)
throttledSetAlerts.cancel()
return
}
allAlerts = allAlerts.concat(alerts)
throttledSetAlerts(allAlerts)
}

setLoading(false)
}, [depKey])

useEffect(() => {
fetch()
}, [depKey])

return {
alerts: renderAlerts,
loading,
error,
}
const units: Record<string, DateTimeUnit> = {
P1D: 'day',
P1W: 'week',
P1M: 'month',
}

export default function AlertMetrics({
serviceID,
}: AlertMetricsProps): JSX.Element {
const now = useMemo(() => DateTime.now(), [])
const minDate = now.minus({ days: MAX_DAY_COUNT - 1 }).startOf('day')
const maxDate = now.endOf('day')

const [params] = useURLParams({
since: minDate.toFormat(DATE_FORMAT),
until: maxDate.toFormat(DATE_FORMAT),
const [svc] = useQuery({
query: 'query Svc($id: ID!) {service(id:$id){id,name}}',
variables: { id: serviceID },
})
const [range] = useURLParam('range', 'P1M')
const [ivl] = useURLParam('interval', 'P1D')
const graphDur = Duration.fromISO(ivl).toISO()

const since = DateTime.fromFormat(params.since, DATE_FORMAT).startOf('day')
const until = DateTime.fromFormat(params.until, DATE_FORMAT).endOf('day')
const unit = units[ivl]
const since = now.minus(Duration.fromISO(range)).startOf(unit)
const until = now.startOf(unit)

const isValidRange =
since >= minDate &&
until >= minDate &&
since <= maxDate &&
until <= maxDate &&
since <= until
const alertsData = useAlerts(serviceID, since.toISO(), until.toISO())
const graphInterval = Interval.fromDateTimes(since, until).toISO()

const alertsData = useAlerts(
serviceID,
since.toISO(),
until.toISO(),
isValidRange,
// useMemo to use same object reference
const metricsOpts: AlertMetricsOpts = useMemo(
() => ({ int: graphInterval, dur: graphDur, alerts: alertsData.alerts }),
[graphInterval, graphDur, alertsData.alerts],
)

if (!isValidRange) {
return <GenericError error='The requested date range is out-of-bounds' />
}
const graphData = useWorker('useAlertMetrics', metricsOpts, [])

if (svc.fetching) return <Spinner />
if (!svc.data?.service?.name) return <ObjectNotFound />

if (alertsData.error) {
return <GenericError error={alertsData.error.message} />
}

const ivl = Interval.fromDateTimes(since, until)

const graphData = ivl.splitBy({ days: 1 }).map((i) => {
const date = i.start.toLocaleString({ month: 'short', day: 'numeric' })
const label = i.start.toLocaleString({
month: 'short',
day: 'numeric',
year: 'numeric',
})

const bucket = alertsData.alerts.filter((a) =>
i.contains(DateTime.fromISO(a.metrics?.closedAt as string)),
)

const escalatedCount = bucket.filter((a) => a.metrics?.escalated).length

return {
date,
label,
count: bucket.length,
nonEscalatedCount: bucket.length - escalatedCount,
escalatedCount,

// get average of a.metrics.timeToClose values
avgTimeToClose: bucket.length
? bucket.reduce((acc, a) => {
if (!a.metrics?.timeToClose) return acc
const timeToClose = Duration.fromISO(a.metrics.timeToClose)
return acc + Math.ceil(timeToClose.as('minutes'))
}, 0) / bucket.length
: 0,

avgTimeToAck: bucket.length
? bucket.reduce((acc, a) => {
if (!a.metrics?.timeToAck) return acc
const timeToAck = Duration.fromISO(a.metrics.timeToAck)
return acc + Math.ceil(timeToAck.as('minutes'))
}, 0) / bucket.length
: 0,
}
})

const daycount = Math.floor(now.diff(since, 'days').plus({ day: 1 }).days)
const dayCount = Math.ceil(until.diff(since, unit).as(unit))

return (
<Grid container spacing={2}>
<Grid item xs={12}>
<Card>
<CardHeader
component='h2'
title={`Daily alert metrics over the past ${daycount} days`}
title={`Daily alert metrics over the past ${dayCount} ${unit}s`}
/>
<CardContent>
<AlertMetricsFilter now={now} />
<AlertMetricsFilter />
<AlertCountGraph data={graphData} />
<AlertAveragesGraph data={graphData} />
<AlertMetricsTable
alerts={alertsData.alerts.map((a) => ({
...a,
...a.metrics,
}))}
alerts={alertsData.alerts}
serviceName={svc.data.service.name}
startTime={since.toFormat('yyyy-MM-dd')}
endTime={until.toFormat('yyyy-MM-dd')}
loading={alertsData.loading}
/>
</CardContent>
Expand Down
56 changes: 0 additions & 56 deletions web/src/app/services/AlertMetrics/AlertMetricsCSV.tsx

This file was deleted.

Loading