Skip to content

Commit

Permalink
feat: include environment type label in feature_toggle_update metrics (
Browse files Browse the repository at this point in the history
…#5809)

This is needed in order to identify what type of an environment a toggle
is updated in. This can be test, development, pre-production or
production.
  • Loading branch information
gardleopard authored Jan 9, 2024
1 parent 2590b4e commit 24b202e
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 18 deletions.
33 changes: 32 additions & 1 deletion src/lib/metrics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { REQUEST_TIME, DB_TIME } from './metric-events';
import {
CLIENT_METRICS,
CLIENT_REGISTER,
FEATURE_ENVIRONMENT_ENABLED,
FEATURE_UPDATED,
} from './types/events';
import { createMetricsMonitor } from './metrics';
Expand All @@ -14,11 +15,14 @@ import { InstanceStatsService } from './features/instance-stats/instance-stats-s
import VersionService from './services/version-service';
import { createFakeGetActiveUsers } from './features/instance-stats/getActiveUsers';
import { createFakeGetProductionChanges } from './features/instance-stats/getProductionChanges';
import { IEnvironmentStore } from './types';
import FakeEnvironmentStore from './features/project-environments/fake-environment-store';

const monitor = createMetricsMonitor();
const eventBus = new EventEmitter();
const prometheusRegister = register;
let eventStore: IEventStore;
let environmentStore: IEnvironmentStore;
let statsService: InstanceStatsService;
let stores;
beforeAll(() => {
Expand All @@ -29,6 +33,8 @@ beforeAll(() => {
});
stores = createStores();
eventStore = stores.eventStore;
environmentStore = new FakeEnvironmentStore();
stores.environmentStore = environmentStore;
const versionService = new VersionService(
stores,
config,
Expand Down Expand Up @@ -93,7 +99,32 @@ test('should collect metrics for updated toggles', async () => {

const metrics = await prometheusRegister.metrics();
expect(metrics).toMatch(
/feature_toggle_update_total\{toggle="TestToggle",project="default",environment="default"\} 1/,
/feature_toggle_update_total\{toggle="TestToggle",project="default",environment="default",environmentType="production"\} 1/,
);
});

test('should set environmentType when toggle is flipped', async () => {
await environmentStore.create({
name: 'testEnvironment',
enabled: true,
type: 'testType',
sortOrder: 1,
});
stores.eventStore.emit(FEATURE_ENVIRONMENT_ENABLED, {
featureName: 'TestToggle',
project: 'default',
environment: 'testEnvironment',
data: { name: 'TestToggle' },
});

// Wait for event to be processed, not nice, but it works.
await new Promise((done) => {
setTimeout(done, 1);
});
const metrics = await prometheusRegister.metrics();

expect(metrics).toMatch(
/feature_toggle_update_total\{toggle="TestToggle",project="default",environment="testEnvironment",environmentType="testType"\} 1/,
);
});

Expand Down
84 changes: 67 additions & 17 deletions src/lib/metrics.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import client from 'prom-client';
import memoizee from 'memoizee';
import EventEmitter from 'events';
import { Knex } from 'knex';
import * as events from './metric-events';
Expand All @@ -19,11 +20,12 @@ import {
CLIENT_REGISTER,
} from './types/events';
import { IUnleashConfig } from './types/option';
import { IUnleashStores } from './types/stores';
import { IEnvironmentStore, IUnleashStores } from './types/stores';
import { hoursToMilliseconds, minutesToMilliseconds } from 'date-fns';
import Timer = NodeJS.Timer;
import { InstanceStatsService } from './features/instance-stats/instance-stats-service';
import { ValidatedClientMetrics } from './services/client-metrics/schema';
import { IEnvironment } from './types';

export default class MetricsMonitor {
timer?: Timer;
Expand All @@ -47,7 +49,15 @@ export default class MetricsMonitor {
return Promise.resolve();
}

const { eventStore } = stores;
const { eventStore, environmentStore } = stores;

const cachedEnvironments: () => Promise<IEnvironment[]> = memoizee(
async () => environmentStore.getAll(),
{
promise: true,
maxAge: hoursToMilliseconds(1),
},
);

client.collectDefaultMetrics();

Expand Down Expand Up @@ -78,7 +88,7 @@ export default class MetricsMonitor {
const featureToggleUpdateTotal = new client.Counter({
name: 'feature_toggle_update_total',
help: 'Number of times a toggle has been updated. Environment label would be "n/a" when it is not available, e.g. when a feature toggle is created.',
labelNames: ['toggle', 'project', 'environment'],
labelNames: ['toggle', 'project', 'environment', 'environmentType'],
});
const featureToggleUsageTotal = new client.Counter({
name: 'feature_toggle_usage_total',
Expand Down Expand Up @@ -360,56 +370,82 @@ export default class MetricsMonitor {
});

eventStore.on(FEATURE_CREATED, ({ featureName, project }) => {
featureToggleUpdateTotal.labels(featureName, project, 'n/a').inc();
featureToggleUpdateTotal
.labels(featureName, project, 'n/a', 'n/a')
.inc();
});
eventStore.on(FEATURE_VARIANTS_UPDATED, ({ featureName, project }) => {
featureToggleUpdateTotal.labels(featureName, project, 'n/a').inc();
featureToggleUpdateTotal
.labels(featureName, project, 'n/a', 'n/a')
.inc();
});
eventStore.on(FEATURE_METADATA_UPDATED, ({ featureName, project }) => {
featureToggleUpdateTotal.labels(featureName, project, 'n/a').inc();
featureToggleUpdateTotal
.labels(featureName, project, 'n/a', 'n/a')
.inc();
});
eventStore.on(FEATURE_UPDATED, ({ featureName, project }) => {
featureToggleUpdateTotal
.labels(featureName, project, 'default')
.labels(featureName, project, 'default', 'production')
.inc();
});
eventStore.on(
FEATURE_STRATEGY_ADD,
({ featureName, project, environment }) => {
async ({ featureName, project, environment }) => {
const environmentType = await this.resolveEnvironmentType(
environment,
cachedEnvironments,
);
featureToggleUpdateTotal
.labels(featureName, project, environment)
.labels(featureName, project, environment, environmentType)
.inc();
},
);
eventStore.on(
FEATURE_STRATEGY_REMOVE,
({ featureName, project, environment }) => {
async ({ featureName, project, environment }) => {
const environmentType = await this.resolveEnvironmentType(
environment,
cachedEnvironments,
);
featureToggleUpdateTotal
.labels(featureName, project, environment)
.labels(featureName, project, environment, environmentType)
.inc();
},
);
eventStore.on(
FEATURE_STRATEGY_UPDATE,
({ featureName, project, environment }) => {
async ({ featureName, project, environment }) => {
const environmentType = await this.resolveEnvironmentType(
environment,
cachedEnvironments,
);
featureToggleUpdateTotal
.labels(featureName, project, environment)
.labels(featureName, project, environment, environmentType)
.inc();
},
);
eventStore.on(
FEATURE_ENVIRONMENT_DISABLED,
({ featureName, project, environment }) => {
async ({ featureName, project, environment }) => {
const environmentType = await this.resolveEnvironmentType(
environment,
cachedEnvironments,
);
featureToggleUpdateTotal
.labels(featureName, project, environment)
.labels(featureName, project, environment, environmentType)
.inc();
},
);
eventStore.on(
FEATURE_ENVIRONMENT_ENABLED,
({ featureName, project, environment }) => {
async ({ featureName, project, environment }) => {
const environmentType = await this.resolveEnvironmentType(
environment,
cachedEnvironments,
);
featureToggleUpdateTotal
.labels(featureName, project, environment)
.labels(featureName, project, environment, environmentType)
.inc();
},
);
Expand Down Expand Up @@ -504,6 +540,20 @@ export default class MetricsMonitor {
// eslint-disable-next-line no-empty
} catch (e) {}
}

async resolveEnvironmentType(
environment: string,
cachedEnvironments: () => Promise<IEnvironment[]>,
): Promise<string> {
const environments = await cachedEnvironments();
const env = environments.find((e) => e.name === environment);

if (env) {
return env.type;
} else {
return 'unknown';
}
}
}
export function createMetricsMonitor(): MetricsMonitor {
return new MetricsMonitor();
Expand Down

0 comments on commit 24b202e

Please sign in to comment.