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:
parent
58f2b5ab1c
commit
e714a7fe2b
@ -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 () => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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) => {
|
||||
|
@ -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 {
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -15,6 +15,7 @@ const ARGS: IAddonConfig = {
|
||||
unleashUrl: 'http://some-url.com',
|
||||
integrationEventsService: {} as IntegrationEventsService,
|
||||
flagResolver: {} as IFlagResolver,
|
||||
eventBus: {} as any,
|
||||
};
|
||||
|
||||
const definition: IAddonDefinition = {
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user