diff --git a/src/featureFlag/FeatureFlagProvider.ts b/src/featureFlag/FeatureFlagProvider.ts index 825bc2af..32d25279 100644 --- a/src/featureFlag/FeatureFlagProvider.ts +++ b/src/featureFlag/FeatureFlagProvider.ts @@ -1,7 +1,8 @@ import { writeFileSync } from 'fs'; import { join } from 'path'; import { LoggerFactory } from '../telemetry/LoggerFactory'; -import { Measure } from '../telemetry/TelemetryDecorator'; +import { ScopedTelemetry } from '../telemetry/ScopedTelemetry'; +import { Measure, Telemetry } from '../telemetry/TelemetryDecorator'; import { Closeable } from '../utils/Closeable'; import { AwsEnv } from '../utils/Environment'; import { readFileIfExists } from '../utils/File'; @@ -12,6 +13,9 @@ import { FeatureFlagSupplier, FeatureFlagConfigKey, TargetedFeatureFlagConfigKey const log = LoggerFactory.getLogger('FeatureFlagProvider'); export class FeatureFlagProvider implements Closeable { + @Telemetry() + private readonly telemetry!: ScopedTelemetry; + private config: unknown; private readonly supplier: FeatureFlagSupplier; @@ -44,6 +48,7 @@ export class FeatureFlagProvider implements Closeable { 5 * 60 * 1000, ); + this.registerGauges(); this.log(); } @@ -81,6 +86,14 @@ export class FeatureFlagProvider implements Closeable { ); } + private registerGauges() { + for (const [key, flag] of this.supplier.featureFlags.entries()) { + this.telemetry.registerGaugeProvider(`featureFlag.${key}`, () => (flag.isEnabled() ? 1 : 0), { + description: `State of ${key} feature flag`, + }); + } + } + close() { this.supplier.close(); clearInterval(this.timeout); diff --git a/tst/unit/featureFlag/FeatureFlagProvider.test.ts b/tst/unit/featureFlag/FeatureFlagProvider.test.ts index f9db8653..02505198 100644 --- a/tst/unit/featureFlag/FeatureFlagProvider.test.ts +++ b/tst/unit/featureFlag/FeatureFlagProvider.test.ts @@ -1,7 +1,9 @@ import { readFileSync } from 'fs'; import { join } from 'path'; -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { FeatureFlagConfigSchema } from '../../../src/featureFlag/FeatureFlagBuilder'; +import { FeatureFlagProvider } from '../../../src/featureFlag/FeatureFlagProvider'; +import { ScopedTelemetry } from '../../../src/telemetry/ScopedTelemetry'; describe('FeatureFlagProvider', () => { it('can parse feature flags', () => { @@ -15,4 +17,42 @@ describe('FeatureFlagProvider', () => { expect(FeatureFlagConfigSchema.parse(JSON.parse(file))).toBeDefined(); }); }); + + describe('gauge registration', () => { + let provider: FeatureFlagProvider; + let registerGaugeProviderSpy: ReturnType; + + beforeEach(() => { + registerGaugeProviderSpy = vi.spyOn(ScopedTelemetry.prototype, 'registerGaugeProvider'); + }); + + afterEach(() => { + provider?.close(); + vi.restoreAllMocks(); + }); + + it('registers gauges for each feature flag', () => { + provider = new FeatureFlagProvider( + () => Promise.resolve({ features: { Constants: { enabled: true } } }), + join(__dirname, '..', '..', '..', 'assets', 'featureFlag', 'alpha.json'), + ); + + expect(registerGaugeProviderSpy).toHaveBeenCalledWith( + 'featureFlag.Constants', + expect.any(Function), + expect.objectContaining({ description: 'State of Constants feature flag' }), + ); + }); + + it('gauge provider reflects current flag state', () => { + provider = new FeatureFlagProvider( + () => Promise.resolve({ features: { Constants: { enabled: false } } }), + join(__dirname, '..', '..', '..', 'assets', 'featureFlag', 'alpha.json'), + ); + + const gaugeProvider = registerGaugeProviderSpy.mock.calls[0][1] as () => number; + // Alpha config has Constants disabled + expect(gaugeProvider()).toBe(0); + }); + }); });