1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat:metrics for outgoing integrations (#7921)

This commit is contained in:
David Leek 2024-08-20 09:00:28 +02:00 committed by GitHub
parent 58f2b5ab1c
commit e714a7fe2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 144 additions and 13 deletions

View File

@ -4,6 +4,7 @@ import noLogger from '../../test/fixtures/no-logger';
import SlackAddon from './slack';
import type { IAddonConfig, IFlagResolver } from '../types';
import type { IntegrationEventsService } from '../services';
import type EventEmitter from 'events';
beforeEach(() => {
nock.disableNetConnect();
@ -16,6 +17,7 @@ const ARGS: IAddonConfig = {
unleashUrl: url,
integrationEventsService: {} as IntegrationEventsService,
flagResolver: {} as IFlagResolver,
eventBus: {} as EventEmitter,
};
test('Does not retry if request succeeds', async () => {

View File

@ -5,6 +5,7 @@ import type { IAddonConfig, IAddonDefinition } from '../types/model';
import type { IEvent } from '../types/events';
import type { IntegrationEventsService } from '../features/integration-events/integration-events-service';
import type { IntegrationEventWriteModel } from '../features/integration-events/integration-events-store';
import type EventEmitter from 'events';
import type { IFlagResolver } from '../types';
export default abstract class Addon {
@ -16,11 +17,18 @@ export default abstract class Addon {
integrationEventsService: IntegrationEventsService;
eventBus: EventEmitter;
flagResolver: IFlagResolver;
constructor(
definition: IAddonDefinition,
{ getLogger, integrationEventsService, flagResolver }: IAddonConfig,
{
getLogger,
integrationEventsService,
flagResolver,
eventBus,
}: IAddonConfig,
) {
this.logger = getLogger(`addon/${definition.name}`);
const { error } = addonDefinitionSchema.validate(definition);
@ -34,6 +42,7 @@ export default abstract class Addon {
this._name = definition.name;
this._definition = definition;
this.integrationEventsService = integrationEventsService;
this.eventBus = eventBus;
this.flagResolver = flagResolver;
}

View File

@ -10,6 +10,7 @@ import DatadogAddon from './datadog';
import noLogger from '../../test/fixtures/no-logger';
import {
type IFlagKey,
serializeDates,
type IAddonConfig,
type IFlagResolver,
@ -24,7 +25,8 @@ const ARGS: IAddonConfig = {
getLogger: noLogger,
unleashUrl: 'http://some-url.com',
integrationEventsService: {} as IntegrationEventsService,
flagResolver: {} as IFlagResolver,
flagResolver: { isEnabled: (expName: IFlagKey) => false } as IFlagResolver,
eventBus: {} as any,
};
jest.mock(

View File

@ -2,7 +2,11 @@ import Addon from './addon';
import definition from './datadog-definition';
import Mustache from 'mustache';
import { type IAddonConfig, serializeDates } from '../types';
import {
type IAddonConfig,
type IFlagResolver,
serializeDates,
} from '../types';
import {
type FeatureEventFormatter,
FeatureEventFormatterMd,
@ -10,6 +14,7 @@ import {
} from './feature-event-formatter-md';
import type { IEvent } from '../types/events';
import type { IntegrationEventState } from '../features/integration-events/integration-events-store';
import { ADDON_EVENTS_HANDLED } from '../metric-events';
interface IDatadogParameters {
url: string;
@ -29,12 +34,15 @@ interface DDRequestBody {
export default class DatadogAddon extends Addon {
private msgFormatter: FeatureEventFormatter;
flagResolver: IFlagResolver;
constructor(config: IAddonConfig) {
super(definition, config);
this.msgFormatter = new FeatureEventFormatterMd(
config.unleashUrl,
LinkStyle.MD,
);
this.flagResolver = config.flagResolver;
}
async handleEvent(
@ -107,6 +115,13 @@ export default class DatadogAddon extends Addon {
state = 'failed';
const failedMessage = `Datadog Events API request failed with status code: ${res.status}.`;
stateDetails.push(failedMessage);
if (this.flagResolver.isEnabled('addonUsageMetrics')) {
this.eventBus.emit(ADDON_EVENTS_HANDLED, {
result: state,
destination: 'datadog',
});
}
this.logger.warn(failedMessage);
}

View File

@ -6,6 +6,7 @@ import {
type IAddonConfig,
type IEvent,
serializeDates,
type IFlagKey,
} from '../types';
import type { Logger } from '../logger';
@ -26,7 +27,8 @@ const ARGS: IAddonConfig = {
getLogger: noLogger,
unleashUrl: 'http://some-url.com',
integrationEventsService: {} as IntegrationEventsService,
flagResolver: {} as IFlagResolver,
flagResolver: { isEnabled: (expName: IFlagKey) => false } as IFlagResolver,
eventBus: {} as any,
};
jest.mock(

View File

@ -6,6 +6,7 @@ import {
type IAddonConfig,
type IEvent,
type IEventType,
type IFlagResolver,
serializeDates,
} from '../types';
import {
@ -16,6 +17,7 @@ import {
import { gzip } from 'node:zlib';
import { promisify } from 'util';
import type { IntegrationEventState } from '../features/integration-events/integration-events-store';
import { ADDON_EVENTS_HANDLED } from '../metric-events';
const asyncGzip = promisify(gzip);
@ -39,12 +41,15 @@ interface INewRelicRequestBody {
export default class NewRelicAddon extends Addon {
private msgFormatter: FeatureEventFormatter;
flagResolver: IFlagResolver;
constructor(config: IAddonConfig) {
super(definition, config);
this.msgFormatter = new FeatureEventFormatterMd(
config.unleashUrl,
LinkStyle.MD,
);
this.flagResolver = config.flagResolver;
}
async handleEvent(
@ -117,6 +122,13 @@ export default class NewRelicAddon extends Addon {
this.logger.warn(failedMessage);
}
if (this.flagResolver.isEnabled('addonUsageMetrics')) {
this.eventBus.emit(ADDON_EVENTS_HANDLED, {
result: state,
destination: 'new-relic',
});
}
this.registerEvent({
integrationId,
state,

View File

@ -3,6 +3,7 @@ import SlackAppAddon from './slack-app';
import { type ChatPostMessageArguments, ErrorCode } from '@slack/web-api';
import {
type IAddonConfig,
type IFlagKey,
type IFlagResolver,
serializeDates,
SYSTEM_USER_ID,
@ -28,7 +29,8 @@ const ARGS: IAddonConfig = {
getLogger,
unleashUrl: 'http://some-url.com',
integrationEventsService: {} as IntegrationEventsService,
flagResolver: {} as IFlagResolver,
flagResolver: { isEnabled: (expName: IFlagKey) => false } as IFlagResolver,
eventBus: {} as any,
};
let postMessage = jest.fn().mockImplementation((options) => {

View File

@ -13,7 +13,11 @@ import {
import Addon from './addon';
import slackAppDefinition from './slack-app-definition';
import { type IAddonConfig, serializeDates } from '../types';
import {
type IAddonConfig,
type IFlagResolver,
serializeDates,
} from '../types';
import {
type FeatureEventFormatter,
FeatureEventFormatterMd,
@ -21,6 +25,7 @@ import {
} from './feature-event-formatter-md';
import type { IEvent } from '../types/events';
import type { IntegrationEventState } from '../features/integration-events/integration-events-store';
import { ADDON_EVENTS_HANDLED } from '../metric-events';
interface ISlackAppAddonParameters {
accessToken: string;
@ -30,6 +35,8 @@ interface ISlackAppAddonParameters {
export default class SlackAppAddon extends Addon {
private msgFormatter: FeatureEventFormatter;
flagResolver: IFlagResolver;
private accessToken?: string;
private slackClient?: WebClient;
@ -40,6 +47,7 @@ export default class SlackAppAddon extends Addon {
args.unleashUrl,
LinkStyle.SLACK,
);
this.flagResolver = args.flagResolver;
}
async handleEvent(
@ -168,6 +176,13 @@ export default class SlackAppAddon extends Addon {
stateDetails.push(eventErrorMessage);
this.logger.warn(eventErrorMessage);
const errorMessage = this.parseError(error);
if (this.flagResolver.isEnabled('addonUsageMetrics')) {
this.eventBus.emit(ADDON_EVENTS_HANDLED, {
result: state,
destination: 'slack-app',
});
}
stateDetails.push(errorMessage);
this.logger.warn(errorMessage, error);
} finally {

View File

@ -11,6 +11,7 @@ import SlackAddon from './slack';
import noLogger from '../../test/fixtures/no-logger';
import {
type IAddonConfig,
type IFlagKey,
type IFlagResolver,
serializeDates,
SYSTEM_USER_ID,
@ -25,7 +26,8 @@ const ARGS: IAddonConfig = {
getLogger: noLogger,
unleashUrl: 'http://some-url.com',
integrationEventsService: {} as IntegrationEventsService,
flagResolver: {} as IFlagResolver,
flagResolver: { isEnabled: (expName: IFlagKey) => false } as IFlagResolver,
eventBus: {} as any,
};
jest.mock(

View File

@ -1,7 +1,11 @@
import Addon from './addon';
import slackDefinition from './slack-definition';
import { type IAddonConfig, serializeDates } from '../types';
import {
type IAddonConfig,
type IFlagResolver,
serializeDates,
} from '../types';
import {
type FeatureEventFormatter,
@ -10,6 +14,7 @@ import {
} from './feature-event-formatter-md';
import type { IEvent } from '../types/events';
import type { IntegrationEventState } from '../features/integration-events/integration-events-store';
import { ADDON_EVENTS_HANDLED } from '../metric-events';
interface ISlackAddonParameters {
url: string;
@ -21,12 +26,15 @@ interface ISlackAddonParameters {
export default class SlackAddon extends Addon {
private msgFormatter: FeatureEventFormatter;
flagResolver: IFlagResolver;
constructor(args: IAddonConfig) {
super(slackDefinition, args);
this.msgFormatter = new FeatureEventFormatterMd(
args.unleashUrl,
LinkStyle.SLACK,
);
this.flagResolver = args.flagResolver;
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@ -121,6 +129,13 @@ export default class SlackAddon extends Addon {
state = 'successWithErrors';
const successWithErrorsMessage = `Some (${failedRequests.length} of ${results.length}) Slack webhook requests failed. Status codes: ${codes}.`;
stateDetails.push(successWithErrorsMessage);
if (this.flagResolver.isEnabled('addonUsageMetrics')) {
this.eventBus.emit(ADDON_EVENTS_HANDLED, {
result: state,
destination: 'slack',
});
}
this.logger.warn(successWithErrorsMessage);
}

View File

@ -12,6 +12,7 @@ import TeamsAddon from './teams';
import noLogger from '../../test/fixtures/no-logger';
import {
type IAddonConfig,
type IFlagKey,
type IFlagResolver,
serializeDates,
SYSTEM_USER_ID,
@ -26,7 +27,8 @@ const ARGS: IAddonConfig = {
getLogger: noLogger,
unleashUrl: 'http://some-url.com',
integrationEventsService: {} as IntegrationEventsService,
flagResolver: {} as IFlagResolver,
flagResolver: { isEnabled: (expName: IFlagKey) => false } as IFlagResolver,
eventBus: {} as any,
};
jest.mock(

View File

@ -1,13 +1,18 @@
import Addon from './addon';
import teamsDefinition from './teams-definition';
import { type IAddonConfig, serializeDates } from '../types';
import {
type IAddonConfig,
type IFlagResolver,
serializeDates,
} from '../types';
import {
type FeatureEventFormatter,
FeatureEventFormatterMd,
} from './feature-event-formatter-md';
import type { IEvent } from '../types/events';
import type { IntegrationEventState } from '../features/integration-events/integration-events-store';
import { ADDON_EVENTS_HANDLED } from '../metric-events';
interface ITeamsParameters {
url: string;
@ -16,9 +21,12 @@ interface ITeamsParameters {
export default class TeamsAddon extends Addon {
private msgFormatter: FeatureEventFormatter;
flagResolver: IFlagResolver;
constructor(args: IAddonConfig) {
super(teamsDefinition, args);
this.msgFormatter = new FeatureEventFormatterMd(args.unleashUrl);
this.flagResolver = args.flagResolver;
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@ -97,6 +105,13 @@ export default class TeamsAddon extends Addon {
state = 'failed';
const failedMessage = `Teams webhook request failed with status code: ${res.status}.`;
stateDetails.push(failedMessage);
if (this.flagResolver.isEnabled('addonUsageMetrics')) {
this.eventBus.emit(ADDON_EVENTS_HANDLED, {
result: state,
destination: 'teams',
});
}
this.logger.warn(failedMessage);
}

View File

@ -7,6 +7,7 @@ import WebhookAddon from './webhook';
import noLogger from '../../test/fixtures/no-logger';
import {
type IAddonConfig,
type IFlagKey,
type IFlagResolver,
serializeDates,
SYSTEM_USER_ID,
@ -21,7 +22,8 @@ const ARGS: IAddonConfig = {
getLogger: noLogger,
unleashUrl: 'http://some-url.com',
integrationEventsService: {} as IntegrationEventsService,
flagResolver: {} as IFlagResolver,
flagResolver: { isEnabled: (expName: IFlagKey) => false } as IFlagResolver,
eventBus: {} as any,
};
jest.mock(

View File

@ -2,16 +2,22 @@ import Mustache from 'mustache';
import Addon from './addon';
import definition from './webhook-definition';
import type { IEvent } from '../types/events';
import { type IAddonConfig, serializeDates } from '../types';
import {
type IAddonConfig,
type IFlagResolver,
serializeDates,
} from '../types';
import type { IntegrationEventState } from '../features/integration-events/integration-events-store';
import {
type FeatureEventFormatter,
FeatureEventFormatterMd,
LinkStyle,
} from './feature-event-formatter-md';
import { ADDON_EVENTS_HANDLED } from '../metric-events';
interface IParameters {
url: string;
serviceName?: string;
bodyTemplate?: string;
contentType?: string;
authorization?: string;
@ -21,12 +27,15 @@ interface IParameters {
export default class Webhook extends Addon {
private msgFormatter: FeatureEventFormatter;
flagResolver: IFlagResolver;
constructor(args: IAddonConfig) {
super(definition, args);
this.msgFormatter = new FeatureEventFormatterMd(
args.unleashUrl,
LinkStyle.MD,
);
this.flagResolver = args.flagResolver;
}
async handleEvent(
@ -94,6 +103,13 @@ export default class Webhook extends Addon {
state = 'failed';
const failedMessage = `Webhook request failed with status code: ${res.status}.`;
stateDetails.push(failedMessage);
if (this.flagResolver.isEnabled('addonUsageMetrics')) {
this.eventBus.emit(ADDON_EVENTS_HANDLED, {
result: state,
destination: 'webhook',
});
}
this.logger.warn(failedMessage);
}

View File

@ -12,6 +12,7 @@ const PROXY_FEATURES_FOR_TOKEN_TIME = 'proxy_features_for_token_time';
const STAGE_ENTERED = 'stage-entered' as const;
const EXCEEDS_LIMIT = 'exceeds-limit' as const;
const REQUEST_ORIGIN = 'request_origin' as const;
const ADDON_EVENTS_HANDLED = 'addon-event-handled' as const;
type MetricEvent =
| typeof REQUEST_TIME
@ -71,6 +72,7 @@ export {
STAGE_ENTERED,
EXCEEDS_LIMIT,
REQUEST_ORIGIN,
ADDON_EVENTS_HANDLED,
type MetricEvent,
type MetricEventPayload,
emitMetricEvent,

View File

@ -359,6 +359,12 @@ export default class MetricsMonitor {
labelNames: ['resource'],
});
const addonEventsHandledCounter = createCounter({
name: 'addon_events_handled',
help: 'Events handled by addons and the result.',
labelNames: ['result', 'destination'],
});
async function collectStaticCounters() {
try {
const stats = await instanceStatsService.getStats();
@ -906,6 +912,10 @@ export default class MetricsMonitor {
projectEnvironmentsDisabled.increment({ project_id: project });
});
eventBus.on(events.ADDON_EVENTS_HANDLED, ({ result, destination }) => {
addonEventsHandledCounter.increment({ result, destination });
});
await this.configureDbMetrics(
db,
eventBus,

View File

@ -15,6 +15,7 @@ const ARGS: IAddonConfig = {
unleashUrl: 'http://some-url.com',
integrationEventsService: {} as IntegrationEventsService,
flagResolver: {} as IFlagResolver,
eventBus: {} as any,
};
const definition: IAddonDefinition = {

View File

@ -64,7 +64,11 @@ export default class AddonService {
getLogger,
server,
flagResolver,
}: Pick<IUnleashConfig, 'getLogger' | 'server' | 'flagResolver'>,
eventBus,
}: Pick<
IUnleashConfig,
'getLogger' | 'server' | 'flagResolver' | 'eventBus'
>,
tagTypeService: TagTypeService,
eventService: EventService,
integrationEventsService,
@ -83,6 +87,7 @@ export default class AddonService {
unleashUrl: server.unleashUrl,
integrationEventsService,
flagResolver,
eventBus,
});
this.sensitiveParams = this.loadSensitiveParams(this.addonProviders);
if (addonStore) {

View File

@ -10,6 +10,7 @@ import type { FeatureSearchEnvironmentSchema } from '../openapi/spec/feature-sea
import type { IntegrationEventsService } from '../features/integration-events/integration-events-service';
import type { IFlagResolver } from './experimental';
import type { Collaborator } from '../features/feature-toggle/types/feature-collaborators-read-model-type';
import type { EventEmitter } from 'events';
export type Operator = (typeof ALL_OPERATORS)[number];
@ -384,6 +385,7 @@ export interface IAddonConfig {
unleashUrl: string;
integrationEventsService: IntegrationEventsService;
flagResolver: IFlagResolver;
eventBus: EventEmitter;
}
export interface IUserWithRole {