Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Pagerduty source and destination #125

Merged
merged 21 commits into from
Nov 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3a395b6
pagerduty source and destination
Roma-Kyrnis Oct 29, 2021
c4a1a86
Merge branch 'main' of github.com:faros-ai/airbyte-connectors into rk…
Roma-Kyrnis Oct 29, 2021
dfbf03a
move test to folder
Roma-Kyrnis Oct 29, 2021
2649b79
update to feedback; since null if full_refresh
Roma-Kyrnis Nov 2, 2021
04b8d64
Merge branch 'main' of github.com:faros-ai/airbyte-connectors into rk…
Roma-Kyrnis Nov 2, 2021
77465a7
since is null if full_refresh
Roma-Kyrnis Nov 2, 2021
e1d77ff
update error text in tests
Roma-Kyrnis Nov 2, 2021
9ea97b2
Merge branch 'main' of github.com:faros-ai/airbyte-connectors into rk…
Roma-Kyrnis Nov 3, 2021
1322da9
add pagerduty config to faros-destination
Roma-Kyrnis Nov 3, 2021
31a8480
update PagerDuty name and names in source test's files
Roma-Kyrnis Nov 3, 2021
6f689da
update test_files/spec.json
Roma-Kyrnis Nov 3, 2021
cccba9b
update acceptance tests; wrapApiError
Roma-Kyrnis Nov 4, 2021
64aa27f
try to fix acceptance tests
Roma-Kyrnis Nov 8, 2021
6305fa1
Merge branch 'main' of github.com:faros-ai/airbyte-connectors into rk…
Roma-Kyrnis Nov 8, 2021
869817b
Merge branch 'main' into rk/add-pagerduty
cjwooo Nov 10, 2021
7156384
Get source acceptance test mostly working
cjwooo Nov 10, 2021
4d3b890
remove application mapping
Roma-Kyrnis Nov 12, 2021
985c177
Merge branch 'main' of github.com:faros-ai/airbyte-connectors into rk…
Roma-Kyrnis Nov 12, 2021
3f9bbee
Merge branch 'rk/add-pagerduty' of github.com:faros-ai/airbyte-connec…
Roma-Kyrnis Nov 12, 2021
bbc7c95
Mark premium stream as expected to be empty
cjwooo Nov 12, 2021
639d968
Merge branch 'main' into rk/add-pagerduty
cjwooo Nov 12, 2021
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
28 changes: 28 additions & 0 deletions destinations/faros-destination/resources/spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,34 @@
"default": false
}
}
},
"pagerduty": {
"title": "PagerDuty",
"description": "Configuration options that apply to records generated by the PagerDuty Source.",
"type": "object",
"properties": {
"application_mapping": {
"type": "object",
"title": "Application Mapping",
"description": "JSON map of PagerDuty service(s) name, to compute platform specific app name and platform name. Used to reference compute_Application object, from an ims_Incident object.",
"default": {},
"examples": [
{
"Aion": {
"name": "aion",
"platform": "ECS"
}
}
]
},
"default_severity": {
"type": "string",
"title": "Default Severity",
"description": "A default severity category if not present",
"pattern": "^(Sev[1-5])?(Custom)?$",
"examples": ["Sev1", "Sev2", "Sev3", "Sev4", "Sev5", "Custom"]
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {AirbyteRecord} from 'faros-airbyte-cdk';

import {Converter, StreamContext} from '../converter';

export interface PagerdutyObject {
readonly id: string;
readonly type: string; // object type of the form <name>_reference
readonly summary: string; // human readable summary
readonly self: string; // API discrete resource url
readonly html_url: string; // PagerDuty web url
}

export enum IncidentSeverityCategory {
Sev1 = 'Sev1',
Sev2 = 'Sev2',
Sev3 = 'Sev3',
Sev4 = 'Sev4',
Sev5 = 'Sev5',
Custom = 'Custom',
}

type ApplicationMapping = Record<string, {name: string; platform?: string}>;

interface PagerdutyConfig {
application_mapping?: ApplicationMapping;
default_severity?: IncidentSeverityCategory;
}

/** PagerDuty converter base */
export abstract class PagerdutyConverter extends Converter {
/** Almost every Pagerduty record have id property. Function will be
* override if record doesn't have id property.
*/
id(record: AirbyteRecord): any {
return record?.record?.data?.id;
}

protected pagerdutyConfig(ctx: StreamContext): PagerdutyConfig {
return ctx.config.source_specific_configs?.pagerduty ?? {};
}

protected applicationMapping(ctx: StreamContext): ApplicationMapping {
return this.pagerdutyConfig(ctx).application_mapping ?? {};
}

protected defaultSeverity(
ctx: StreamContext
): IncidentSeverityCategory | null {
return this.pagerdutyConfig(ctx).default_severity ?? null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {AirbyteRecord} from 'faros-airbyte-cdk';
import {Utils} from 'faros-feeds-sdk';

import {DestinationModel, DestinationRecord, StreamContext} from '../converter';
import {PagerdutyConverter} from './common';

interface IncidentEventType {
category: IncidentEventTypeCategory;
detail: string;
}

enum IncidentEventTypeCategory {
Created = 'Created',
Acknowledged = 'Acknowledged',
Resolved = 'Resolved',
Custom = 'Custom',
}

export class PagerdutyIncidentLogEntries extends PagerdutyConverter {
readonly destinationModels: ReadonlyArray<DestinationModel> = ['ims_IncidentEvent'];

convert(
record: AirbyteRecord,
ctx: StreamContext
): ReadonlyArray<DestinationRecord> {
const source = this.streamName.source;
const event = record.record.data;

return [
{
model: 'ims_IncidentEvent',
record: {
uid: event.id,
type: this.eventType(event.type),
createdAt: Utils.toDate(event.created_at),
detail: event.summary,
incident: {uid: event.incident.id, source},
},
},
];
}

private eventType(logEntryType: string): IncidentEventType {
const typeRef = logEntryType.split('_')[0]; // i.e. "resolve" of "resolve_log_entry"
let eventTypeCategory;
switch (typeRef.toLowerCase()) {
case 'trigger':
eventTypeCategory = IncidentEventTypeCategory.Created;
break;
case 'acknowledge':
eventTypeCategory = IncidentEventTypeCategory.Acknowledged;
break;
case 'resolve':
eventTypeCategory = IncidentEventTypeCategory.Resolved;
break;
default:
eventTypeCategory = IncidentEventTypeCategory.Custom;
break;
}
return {category: eventTypeCategory, detail: logEntryType};
}
}
152 changes: 152 additions & 0 deletions destinations/faros-destination/src/converters/pagerduty/incidents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import {AirbyteLogger, AirbyteRecord} from 'faros-airbyte-cdk';
import {Utils} from 'faros-feeds-sdk';

import {DestinationModel, DestinationRecord, StreamContext} from '../converter';
import {PagerdutyConverter, PagerdutyObject} from './common';

enum IncidentStatusCategory {
Created = 'Created',
Identified = 'Identified',
Investigating = 'Investigating',
Resolved = 'Resolved',
Custom = 'Custom',
}

type IncidentUrgency = 'high' | 'low'; //PagerDuty only has these two priorities
type IncidentState = 'triggered' | 'acknowledged' | 'resolved';

interface Acknowledgement {
at: string; //date-time
acknowledger: PagerdutyObject;
}

export class PagerdutyIncidents extends PagerdutyConverter {
private readonly logger = new AirbyteLogger();

readonly destinationModels: ReadonlyArray<DestinationModel> = [
'compute_Application',
'ims_Incident',
'ims_IncidentApplicationImpact',
'ims_IncidentAssignment',
];

convert(
record: AirbyteRecord,
ctx: StreamContext
): ReadonlyArray<DestinationRecord> {
const source = this.streamName.source;
const incident = record.record.data;
const res: DestinationRecord[] = [];
const incidentRef = {uid: incident.id, source};
const lastUpdated = Utils.toDate(incident.last_status_change_at);

let acknowledgedAt, resolvedAt;
if (incident.status === 'acknowledged') {
if (!incident.acknowledgements || !incident.acknowledgements.length) {
this.logger.warn(
`Incident ${incident.id} acknowledged, but acknowledger info missing`
);
} else {
// find first acknowledgement
acknowledgedAt = Utils.toDate(
incident.acknowledgements
.map((ack: Acknowledgement) => ack.at)
.sort()[0]
);
}
} else if (incident.status === 'resolved') {
resolvedAt = lastUpdated;
}

res.push({
// We are explicitly passing __Upsert command here with at := 0,
// to allow updating Incident severity from prioritiesResource stream
// in the same revision
model: 'ims_Incident__Upsert',
record: {
at: 0,
data: {
...incidentRef,
title: incident.title,
description: incident.description,
url: incident.self,
createdAt: Utils.toDate(incident.created_at),
updatedAt: resolvedAt,
acknowledgedAt: acknowledgedAt,
resolvedAt: resolvedAt,
priority: this.incidentPriority(incident.urgency),
status: this.incidentState(incident.status),
},
},
});

for (const assignment of incident.assignments) {
const assignee = {uid: assignment.assignee.id, source};
res.push({
model: 'ims_IncidentAssignment',
record: {
incident: incidentRef,
assignee,
},
});
}

const applicationMapping = this.applicationMapping(ctx);
let application = {
name: incident.service.summary,
platform: '',
};
// if we have an app mapping specified
if (
incident.service.summary in applicationMapping &&
applicationMapping[incident.service.summary].name
) {
const mappedApp = applicationMapping[incident.service.summary];
application = {
name: mappedApp.name,
platform: mappedApp.platform ?? application.platform,
};
}
res.push({model: 'compute_Application', record: application});

res.push({
model: 'ims_IncidentApplicationImpact',
record: {
incident: incidentRef,
application,
},
});

return res;
}

private incidentPriority(
incidentUrgency: IncidentUrgency
): Record<string, string> {
const detail = incidentUrgency;
switch (incidentUrgency.toLowerCase()) {
case 'high':
return {category: 'High', detail};
case 'low':
return {category: 'Low', detail};
default:
return {category: 'Custom', detail};
}
}

private incidentState(incidentStatus: IncidentState): {
category: string;
detail: string;
} {
const detail = incidentStatus;
switch (incidentStatus) {
case 'resolved':
return {category: IncidentStatusCategory.Resolved, detail};
case 'acknowledged':
return {category: IncidentStatusCategory.Investigating, detail};
case 'triggered':
default:
return {category: IncidentStatusCategory.Created, detail};
}
}
}
Loading