diff --git a/plugins/splunk-on-call/README.md b/plugins/splunk-on-call/README.md index 73ee05c181..89fd45adc0 100644 --- a/plugins/splunk-on-call/README.md +++ b/plugins/splunk-on-call/README.md @@ -56,6 +56,7 @@ In `app-config.yaml`: ```yaml splunkOnCall: username: + eventsRestEndpoint: ``` The user supplied must be a valid Splunk On-Call user and a member of your organization. diff --git a/plugins/splunk-on-call/src/api/client.ts b/plugins/splunk-on-call/src/api/client.ts index 501dad80a2..616ebb7023 100644 --- a/plugins/splunk-on-call/src/api/client.ts +++ b/plugins/splunk-on-call/src/api/client.ts @@ -31,7 +31,6 @@ import { RequestOptions, ListUserResponse, EscalationPolicyResponse, - PatchIncidentRequest, } from './types'; export class UnauthorizedError extends Error {} @@ -45,8 +44,11 @@ export class SplunkOnCallClient implements SplunkOnCallApi { static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) { const usernameFromConfig: string | null = configApi.getOptionalString('splunkOnCall.username') || null; + const eventsRestEndpoint: string | null = + configApi.getOptionalString('splunkOnCall.eventsRestEndpoint') || null; return new SplunkOnCallClient({ username: usernameFromConfig, + eventsRestEndpoint, discoveryApi, }); } @@ -80,50 +82,6 @@ export class SplunkOnCallClient implements SplunkOnCallApi { return teams; } - async acknowledgeIncident({ - incidentNames, - }: PatchIncidentRequest): Promise { - const options = { - method: 'PATCH', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - userName: this.config.username, - incidentNames, - }), - }; - - const url = `${await this.config.discoveryApi.getBaseUrl( - 'proxy', - )}/splunk-on-call/v1/incidents/ack`; - - return this.request(url, options); - } - - async resolveIncident({ - incidentNames, - }: PatchIncidentRequest): Promise { - const options = { - method: 'PATCH', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - userName: this.config.username, - incidentNames, - }), - }; - - const url = `${await this.config.discoveryApi.getBaseUrl( - 'proxy', - )}/splunk-on-call/v1/incidents/resolve`; - - return this.request(url, options); - } - async getUsers(): Promise { const url = `${await this.config.discoveryApi.getBaseUrl( 'proxy', @@ -142,19 +100,22 @@ export class SplunkOnCallClient implements SplunkOnCallApi { return policies; } - async triggerAlarm({ - summary, - details, - userName, - targets, - isMultiResponder, + async incidentAction({ + routingKey, + incidentType, + incidentId, + incidentDisplayName, + incidentMessage, + incidentStartTime, }: TriggerAlarmRequest): Promise { const body = JSON.stringify({ - summary, - details, - userName: this.config.username || userName, - targets, - isMultiResponder, + message_type: incidentType, + ...(incidentId ? { entity_id: incidentId } : {}), + ...(incidentDisplayName + ? { entity_display_name: incidentDisplayName } + : {}), + ...(incidentMessage ? { state_message: incidentMessage } : {}), + ...(incidentStartTime ? { state_start_time: incidentStartTime } : {}), }); const options = { @@ -166,9 +127,7 @@ export class SplunkOnCallClient implements SplunkOnCallApi { body, }; - const url = `${await this.config.discoveryApi.getBaseUrl( - 'proxy', - )}/splunk-on-call/v1/incidents`; + const url = `${this.config.eventsRestEndpoint}/${routingKey}`; return this.request(url, options); } diff --git a/plugins/splunk-on-call/src/api/types.ts b/plugins/splunk-on-call/src/api/types.ts index 500f2a6291..b4007a9051 100644 --- a/plugins/splunk-on-call/src/api/types.ts +++ b/plugins/splunk-on-call/src/api/types.ts @@ -23,22 +23,20 @@ import { } from '../components/types'; import { DiscoveryApi } from '@backstage/core'; -export enum TargetType { - UserValue = 'User', - EscalationPolicyValue = 'EscalationPolicy', -} - -export type IncidentTarget = { - type: TargetType; - slug: string; -}; +export type MessageType = + | 'CRITICAL' + | 'WARNING' + | 'ACKNOWLEDGEMENT' + | 'INFO' + | 'RECOVERY'; export type TriggerAlarmRequest = { - targets: IncidentTarget[]; - details: string; - summary: string; - userName: string; - isMultiResponder?: boolean; + routingKey?: string; + incidentType: MessageType; + incidentId?: string; + incidentDisplayName?: string; + incidentMessage?: string; + incidentStartTime?: number; }; export interface SplunkOnCallApi { @@ -53,19 +51,9 @@ export interface SplunkOnCallApi { getOnCallUsers(): Promise; /** - * Triggers an incident to specific users and/or specific teams. + * Triggers-Resolves-Acknowledge an incident. */ - triggerAlarm(request: TriggerAlarmRequest): Promise; - - /** - * Resolves an incident. - */ - resolveIncident(request: PatchIncidentRequest): Promise; - - /** - * Acknowledge an incident. - */ - acknowledgeIncident(request: PatchIncidentRequest): Promise; + incidentAction(request: TriggerAlarmRequest): Promise; /** * Get a list of users for your organization. @@ -83,11 +71,6 @@ export interface SplunkOnCallApi { getEscalationPolicies(): Promise; } -export type PatchIncidentRequest = { - incidentNames: string[]; - message?: string; -}; - export type EscalationPolicyResponse = { policies: EscalationPolicyInfo[]; }; @@ -107,6 +90,7 @@ export type OnCallsResponse = { export type ClientApiConfig = { username: string | null; + eventsRestEndpoint: string | null; discoveryApi: DiscoveryApi; }; diff --git a/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx b/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx index 1bf233de86..7bd8081226 100644 --- a/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx +++ b/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx @@ -39,7 +39,7 @@ import { Incident, IncidentPhase } from '../types'; import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; import { splunkOnCallApiRef } from '../../api/client'; import { useAsyncFn } from 'react-use'; -import { PatchIncidentRequest } from '../../api/types'; +import { TriggerAlarmRequest } from '../../api/types'; const useStyles = makeStyles({ denseListIcon: { @@ -61,6 +61,7 @@ const useStyles = makeStyles({ }); type Props = { + team: string; incident: Incident; onIncidentAction: () => void; }; @@ -93,20 +94,24 @@ const incidentPhaseTooltip = (currentPhase: IncidentPhase) => { const IncidentAction = ({ currentPhase, - incidentNames, + incidentId, resolveAction, acknowledgeAction, }: { currentPhase: string; - incidentNames: string[]; - resolveAction: (args: PatchIncidentRequest) => void; - acknowledgeAction: (args: PatchIncidentRequest) => void; + incidentId: string; + resolveAction: (args: TriggerAlarmRequest) => void; + acknowledgeAction: (args: TriggerAlarmRequest) => void; }) => { switch (currentPhase) { case 'UNACKED': return ( - acknowledgeAction({ incidentNames })}> + + acknowledgeAction({ incidentId, incidentType: 'ACKNOWLEDGEMENT' }) + } + > @@ -114,7 +119,11 @@ const IncidentAction = ({ case 'ACKED': return ( - resolveAction({ incidentNames })}> + + resolveAction({ incidentId, incidentType: 'RECOVERY' }) + } + > @@ -124,7 +133,11 @@ const IncidentAction = ({ } }; -export const IncidentListItem = ({ incident, onIncidentAction }: Props) => { +export const IncidentListItem = ({ + incident, + onIncidentAction, + team, +}: Props) => { const classes = useStyles(); const duration = new Date().getTime() - new Date(incident.startTime!).getTime(); @@ -136,17 +149,26 @@ export const IncidentListItem = ({ incident, onIncidentAction }: Props) => { const hasBeenManuallyTriggered = incident.monitorName?.includes('vouser-'); - const user = hasBeenManuallyTriggered - ? incident.monitorName?.replace('vouser-', '') - : incident.monitorName; + const source = () => { + if (hasBeenManuallyTriggered) { + return incident.monitorName?.replace('vouser-', ''); + } + if (incident.monitorType === 'API') { + return '{ REST }'; + } + + return incident.monitorName; + }; const [ { value: resolveValue, error: resolveError }, handleResolveIncident, ] = useAsyncFn( - async ({ incidentNames }: PatchIncidentRequest) => - await api.resolveIncident({ - incidentNames, + async ({ incidentId, incidentType }: TriggerAlarmRequest) => + await api.incidentAction({ + routingKey: team, + incidentType, + incidentId, }), ); @@ -154,9 +176,11 @@ export const IncidentListItem = ({ incident, onIncidentAction }: Props) => { { value: acknowledgeValue, error: acknowledgeError }, handleAcknowledgeIncident, ] = useAsyncFn( - async ({ incidentNames }: PatchIncidentRequest) => - await api.acknowledgeIncident({ - incidentNames, + async ({ incidentId, incidentType }: TriggerAlarmRequest) => + await api.incidentAction({ + routingKey: team, + incidentType, + incidentId, }), ); @@ -211,7 +235,8 @@ export const IncidentListItem = ({ incident, onIncidentAction }: Props) => { }} secondary={ - Created {createdAt} {user && `by ${user}`} + #{incident.incidentNumber} - Created {createdAt}{' '} + {source() && `by ${source()}`} } /> @@ -220,7 +245,7 @@ export const IncidentListItem = ({ incident, onIncidentAction }: Props) => { diff --git a/plugins/splunk-on-call/src/components/Incident/Incidents.tsx b/plugins/splunk-on-call/src/components/Incident/Incidents.tsx index e1a79314b9..507020f0a5 100644 --- a/plugins/splunk-on-call/src/components/Incident/Incidents.tsx +++ b/plugins/splunk-on-call/src/components/Incident/Incidents.tsx @@ -33,6 +33,10 @@ export const Incidents = ({ refreshIncidents, team }: Props) => { const [{ value: incidents, loading, error }, getIncidents] = useAsyncFn( async () => { + // For some reason the changes applied to incidents (trigger-resolve-acknowledge) + // may take some time to actually be applied after receiving the response from the server. + // The timeout compensates for this latency. + await new Promise(resolve => setTimeout(resolve, 2000)); const allIncidents = await api.getIncidents(); const teams = await api.getTeams(); const teamSlug = teams.find(teamValue => teamValue.name === team)?.slug; @@ -71,6 +75,7 @@ export const Incidents = ({ refreshIncidents, team }: Props) => { getIncidents()} key={index} + team={team} incident={incident} /> ))} diff --git a/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx index 84d94d60d3..e2cc642c6a 100644 --- a/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx @@ -74,6 +74,16 @@ export const MissingUsername = () => ( ); +export const MissingEventsRestEndpoint = () => ( + + + +); + export const isPluginApplicableToEntity = (entity: Entity) => Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_TEAM]); @@ -89,7 +99,10 @@ export const SplunkOnCallCard = ({ entity }: Props) => { const [refreshIncidents, setRefreshIncidents] = useState(false); const team = entity.metadata.annotations![SPLUNK_ON_CALL_TEAM]; - const username = config.getOptionalString('splunkOnCall.username'); + const username = config.getOptionalString('splunkOnCall.username') || null; + + const eventsRestEndpoint = + config.getOptionalString('splunkOnCall.eventsRestEndpoint') || null; const handleRefresh = useCallback(() => { setRefreshIncidents(x => !x); @@ -140,15 +153,19 @@ export const SplunkOnCallCard = ({ entity }: Props) => { return ; } + if (!eventsRestEndpoint) { + return ; + } + return ( <> {users?.usersHashMap && team && ( )} - {users && incidentCreator && ( + {incidentCreator && ( void; @@ -79,7 +69,7 @@ const useStyles = makeStyles((theme: Theme) => ); export const TriggerDialog = ({ - users, + team, incidentCreator, showDialog, handleDialog, @@ -89,72 +79,46 @@ export const TriggerDialog = ({ const api = useApi(splunkOnCallApiRef); const classes = useStyles(); - const [userTargets, setUserTargets] = useState([]); - const [policyTargets, setPolicyTargets] = useState([]); - const [detailsValue, setDetails] = useState(''); - const [summaryValue, setSummary] = useState(''); - const [isMultiResponderValue, setIsMultiResponder] = useState('1'); + const [incidentType, setIncidentType] = useState(''); + const [incidentId, setIncidentId] = useState(); + const [incidentDisplayName, setIncidentDisplayName] = useState(''); + const [incidentMessage, setIncidentMessage] = useState(''); + const [incidentStartTime, setIncidentStartTime] = useState(); const [ { value, loading: triggerLoading, error: triggerError }, handleTriggerAlarm, ] = useAsyncFn( - async ( - summary: string, - details: string, - userName: string, - targets: IncidentTarget[], - isMultiResponder: boolean, - ) => - await api.triggerAlarm({ - summary, - details, - userName, - targets, - isMultiResponder, - }), + async (params: TriggerAlarmRequest) => await api.incidentAction(params), ); - const { - value: policies, - loading: policiesLoaading, - error: policiesError, - } = useAsync(async () => { - const allPolicies = await api.getEscalationPolicies(); - return allPolicies; - }); - - const handleUserTargets = (event: React.ChangeEvent<{ value: unknown }>) => { - setUserTargets(event.target.value as string[]); + const handleIncidentType = (event: React.ChangeEvent<{ value: unknown }>) => { + setIncidentType(event.target.value as string); }; - const handlePolicyTargets = ( - event: React.ChangeEvent<{ value: unknown }>, + const handleIncidentId = (event: React.ChangeEvent) => { + setIncidentId(event.target.value as string); + }; + + const handleIncidentDisplayName = ( + event: React.ChangeEvent, ) => { - setPolicyTargets(event.target.value as string[]); + setIncidentDisplayName(event.target.value); }; - const detailsChanged = (event: React.ChangeEvent) => { - setDetails(event.target.value); - }; - - const summaryChanged = (event: React.ChangeEvent) => { - setSummary(event.target.value); - }; - - const isMultiResponderChanged = ( - event: React.ChangeEvent<{ value: unknown }>, + const handleIncidentMessage = ( + event: React.ChangeEvent, ) => { - setIsMultiResponder(event.target.value as string); + setIncidentMessage(event.target.value); }; - const targets = (): IncidentTarget[] => [ - ...userTargets.map(user => ({ slug: user, type: TargetType.UserValue })), - ...policyTargets.map(user => ({ - slug: user, - type: TargetType.EscalationPolicyValue, - })), - ]; + const handleIncidentStartTime = ( + event: React.ChangeEvent, + ) => { + const dateTime = new Date(event.target.value).getTime(); + const dateTimeInSeconds = Math.floor(dateTime / 1000); + setIncidentStartTime(dateTimeInSeconds); + }; useEffect(() => { if (value) { @@ -203,132 +167,58 @@ export const TriggerDialog = ({ automatically be amended to the alarm so that the receiver can reach out to you if necessary. -
- - Select the targets - -
- - Select Users - - - - Select Teams / Policies - - -
-
- - Acknowledge Behavior - + Incident type + + @@ -337,20 +227,21 @@ export const TriggerDialog = ({ id="trigger" color="secondary" disabled={ - !detailsValue || - !summaryValue || - (!userTargets.length && !policyTargets.length) || + !incidentType.length || + !incidentDisplayName || + !incidentMessage || triggerLoading } variant="contained" onClick={() => - handleTriggerAlarm( - summaryValue, - detailsValue, - incidentCreator.username!, - targets(), - !!Number(isMultiResponderValue), - ) + handleTriggerAlarm({ + routingKey: team, + incidentType, + incidentDisplayName, + incidentMessage, + ...(incidentId ? { incidentId } : {}), + ...(incidentStartTime ? { incidentStartTime } : {}), + } as TriggerAlarmRequest) } endIcon={triggerLoading && } > diff --git a/plugins/splunk-on-call/src/components/types.ts b/plugins/splunk-on-call/src/components/types.ts index de5718cb28..9b2af1cddd 100644 --- a/plugins/splunk-on-call/src/components/types.ts +++ b/plugins/splunk-on-call/src/components/types.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { IncidentTarget } from '../api/types'; - export type Team = { name?: string; slug?: string; @@ -68,14 +66,6 @@ export type User = { _selfUrl?: string; }; -export type CreateIncidentRequest = { - summary: string; - details: string; - userName: string; - targets: IncidentTarget; - isMultiResponder: boolean; -}; - export type IncidentPhase = 'UNACKED' | 'ACKED' | 'RESOLVED'; export type Incident = { @@ -88,7 +78,7 @@ export type Incident = { alertCount?: number; lastAlertTime?: string; lastAlertId?: string; - entityId?: string; + entityId: string; host?: string; service?: string; pagedUsers?: string[];