Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
50 changes: 44 additions & 6 deletions src/AzureAppConfigurationImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import { IKeyValueAdapter } from "./IKeyValueAdapter.js";
import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter.js";
import { DEFAULT_REFRESH_INTERVAL_IN_MS, MIN_REFRESH_INTERVAL_IN_MS } from "./RefreshOptions.js";
import { Disposable } from "./common/disposable.js";
import { FEATURE_FLAGS_KEY_NAME, FEATURE_MANAGEMENT_KEY_NAME } from "./featureManagement/constants.js";
import { FEATURE_FLAGS_KEY_NAME, FEATURE_MANAGEMENT_KEY_NAME, CONDITIONS_KEY_NAME, CLIENT_FILTERS_KEY_NAME, TELEMETRY_KEY_NAME, VARIANTS_KEY_NAME, ALLOCATION_KEY_NAME, SEED_KEY_NAME, NAME_KEY_NAME, ENABLED_KEY_NAME } from "./featureManagement/constants.js";
import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js";
import { RefreshTimer } from "./refresh/RefreshTimer.js";
import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js";
import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOptions.js";
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";

type PagedSettingSelector = SettingSelector & {
Expand All @@ -38,6 +39,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
#client: AppConfigurationClient;
#options: AzureAppConfigurationOptions | undefined;
#isInitialLoadCompleted: boolean = false;
#featureFlagTracing: FeatureFlagTracingOptions | undefined;

// Refresh
#refreshInProgress: boolean = false;
Expand Down Expand Up @@ -66,6 +68,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {

// Enable request tracing if not opt-out
this.#requestTracingEnabled = requestTracingEnabled();
if (this.#requestTracingEnabled) {
this.#featureFlagTracing = new FeatureFlagTracingOptions();
}

if (options?.trimKeyPrefixes) {
this.#sortedTrimKeyPrefixes = [...options.trimKeyPrefixes].sort((a, b) => b.localeCompare(a));
Expand Down Expand Up @@ -175,7 +180,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
return {
requestTracingEnabled: this.#requestTracingEnabled,
initialLoadCompleted: this.#isInitialLoadCompleted,
appConfigOptions: this.#options
appConfigOptions: this.#options,
featureFlagTracingOptions: this.#featureFlagTracing
};
}

Expand Down Expand Up @@ -257,8 +263,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
}

async #loadFeatureFlags() {
// Temporary map to store feature flags, key is the key of the setting, value is the raw value of the setting
const featureFlagsMap = new Map<string, any>();
const featureFlagSettings: ConfigurationSetting[] = [];
for (const selector of this.#featureFlagSelectors) {
const listOptions: ListConfigurationSettingsOptions = {
keyFilter: `${featureFlagPrefix}${selector.keyFilter}`,
Expand All @@ -275,15 +280,21 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
pageEtags.push(page.etag ?? "");
for (const setting of page.items) {
if (isFeatureFlag(setting)) {
featureFlagsMap.set(setting.key, setting.value);
featureFlagSettings.push(setting);
}
}
}
selector.pageEtags = pageEtags;
}

if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) {
this.#featureFlagTracing.resetFeatureFlagTracing();
}

// parse feature flags
const featureFlags = Array.from(featureFlagsMap.values()).map(rawFlag => JSON.parse(rawFlag));
const featureFlags = await Promise.all(
featureFlagSettings.map(setting => this.#parseFeatureFlag(setting))
);

// feature_management is a reserved key, and feature_flags is an array of feature flags
this.#configMap.set(FEATURE_MANAGEMENT_KEY_NAME, { [FEATURE_FLAGS_KEY_NAME]: featureFlags });
Expand Down Expand Up @@ -546,6 +557,33 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
}
return response;
}

async #parseFeatureFlag(setting: ConfigurationSetting<string>): Promise<any> {
const rawFlag = setting.value;
if (rawFlag === undefined) {
throw new Error("The value of configuration setting cannot be undefined.");
}
const featureFlag = JSON.parse(rawFlag);
if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) {
if (featureFlag[CONDITIONS_KEY_NAME] &&
featureFlag[CONDITIONS_KEY_NAME][CLIENT_FILTERS_KEY_NAME] &&
Array.isArray(featureFlag[CONDITIONS_KEY_NAME][CLIENT_FILTERS_KEY_NAME])) {
for (const filter of featureFlag[CONDITIONS_KEY_NAME][CLIENT_FILTERS_KEY_NAME]) {
this.#featureFlagTracing.updateFeatureFilterTracing(filter[NAME_KEY_NAME]);
}
}
if (featureFlag[VARIANTS_KEY_NAME] && Array.isArray(featureFlag[VARIANTS_KEY_NAME])) {
this.#featureFlagTracing.notifyMaxVariants(featureFlag[VARIANTS_KEY_NAME].length);
}
if (featureFlag[TELEMETRY_KEY_NAME] && featureFlag[TELEMETRY_KEY_NAME][ENABLED_KEY_NAME]) {
this.#featureFlagTracing.usesTelemetry = true;
}
if (featureFlag[ALLOCATION_KEY_NAME] && featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME]) {
this.#featureFlagTracing.usesSeed = true;
}
}
return featureFlag;
}
}

function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] {
Expand Down
13 changes: 12 additions & 1 deletion src/featureManagement/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,15 @@
// Licensed under the MIT license.

export const FEATURE_MANAGEMENT_KEY_NAME = "feature_management";
export const FEATURE_FLAGS_KEY_NAME = "feature_flags";
export const FEATURE_FLAGS_KEY_NAME = "feature_flags";
export const CONDITIONS_KEY_NAME = "conditions";
export const CLIENT_FILTERS_KEY_NAME = "client_filters";
export const TELEMETRY_KEY_NAME = "telemetry";
export const VARIANTS_KEY_NAME = "variants";
export const ALLOCATION_KEY_NAME = "allocation";
export const SEED_KEY_NAME = "seed";
export const NAME_KEY_NAME = "name";
export const ENABLED_KEY_NAME = "enabled";

export const TIME_WINDOW_FILTER_NAMES = ["TimeWindow", "Microsoft.TimeWindow", "TimeWindowFilter", "Microsoft.TimeWindowFilter"];
export const TARGETING_FILTER_NAMES = ["Targeting", "Microsoft.Targeting", "TargetingFilter", "Microsoft.TargetingFilter"];
95 changes: 95 additions & 0 deletions src/requestTracing/FeatureFlagTracingOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { TIME_WINDOW_FILTER_NAMES, TARGETING_FILTER_NAMES } from "../featureManagement/constants.js";
import { CUSTOM_FILTER_KEY, TIME_WINDOW_FILTER_KEY, TARGETING_FILTER_KEY, FF_SEED_USED_TAG, FF_TELEMETRY_USED_TAG, DELIMITER } from "./constants.js";

/**
* Tracing for tracking feature flag usage.
*/
export class FeatureFlagTracingOptions {
/**
* Built-in feature filter usage.
*/
usesCustomFilter: boolean = false;
usesTimeWindowFilter: boolean = false;
usesTargetingFilter: boolean = false;
usesTelemetry: boolean = false;
usesSeed: boolean = false;
maxVariants: number = 0;

resetFeatureFlagTracing(): void {
this.usesCustomFilter = false;
this.usesTimeWindowFilter = false;
this.usesTargetingFilter = false;
this.usesTelemetry = false;
this.usesSeed = false;
this.maxVariants = 0;
}

updateFeatureFilterTracing(filterName: string): void {
if (TIME_WINDOW_FILTER_NAMES.some(name => name === filterName)) {
this.usesTimeWindowFilter = true;
} else if (TARGETING_FILTER_NAMES.some(name => name === filterName)) {
this.usesTargetingFilter = true;
} else {
this.usesCustomFilter = true;
}
}

notifyMaxVariants(currentFFTotalVariants: number): void {
if (currentFFTotalVariants > this.maxVariants) {
this.maxVariants = currentFFTotalVariants;
}
}

usesAnyFeatureFilter(): boolean {
return this.usesCustomFilter || this.usesTimeWindowFilter || this.usesTargetingFilter;
}

usesAnyTracingFeature() {
return this.usesSeed || this.usesTelemetry;
}

createFeatureFiltersString(): string {
if (!this.usesAnyFeatureFilter()) {
return "";
}

let result: string = "";
if (this.usesCustomFilter) {
result += CUSTOM_FILTER_KEY;
}
if (this.usesTimeWindowFilter) {
if (result !== "") {
result += DELIMITER;
}
result += TIME_WINDOW_FILTER_KEY;
}
if (this.usesTargetingFilter) {
if (result !== "") {
result += DELIMITER;
}
result += TARGETING_FILTER_KEY;
}
return result;
}

createFeaturesString(): string {
if (!this.usesAnyTracingFeature()) {
return "";
}

let result: string = "";
if (this.usesSeed) {
result += FF_SEED_USED_TAG;
}
if (this.usesTelemetry) {
if (result !== "") {
result += DELIMITER;
}
result += FF_TELEMETRY_USED_TAG;
}
return result;
}
}
15 changes: 14 additions & 1 deletion src/requestTracing/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const CONTAINER_APP_ENV_VAR = "CONTAINER_APP_NAME";
export const KUBERNETES_ENV_VAR = "KUBERNETES_PORT";
export const SERVICE_FABRIC_ENV_VAR = "Fabric_NodeName"; // See: https://docs.microsoft.com/en-us/azure/service-fabric/service-fabric-environment-variables-reference

// Request Type
// Request type
export const REQUEST_TYPE_KEY = "RequestType";
export enum RequestType {
STARTUP = "Startup",
Expand All @@ -46,3 +46,16 @@ export enum RequestType {

// Tag names
export const KEY_VAULT_CONFIGURED_TAG = "UsesKeyVault";

// Feature flag usage tracing
export const FEATURE_FILTER_TYPE_KEY = "Filter";
export const CUSTOM_FILTER_KEY = "CSTM";
export const TIME_WINDOW_FILTER_KEY = "TIME";
export const TARGETING_FILTER_KEY = "TRGT";

export const FF_TELEMETRY_USED_TAG = "Telemetry";
export const FF_MAX_VARIANTS_KEY = "MaxVariants";
export const FF_SEED_USED_TAG = "Seed";
export const FF_FEATURES_KEY = "FFFeatures";

export const DELIMITER = "+";
24 changes: 19 additions & 5 deletions src/requestTracing/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@

import { AppConfigurationClient, ConfigurationSettingId, GetConfigurationSettingOptions, ListConfigurationSettingsOptions } from "@azure/app-configuration";
import { AzureAppConfigurationOptions } from "../AzureAppConfigurationOptions.js";
import { FeatureFlagTracingOptions } from "./FeatureFlagTracingOptions.js";
import {
AZURE_FUNCTION_ENV_VAR,
AZURE_WEB_APP_ENV_VAR,
CONTAINER_APP_ENV_VAR,
DEV_ENV_VAL,
ENV_AZURE_APP_CONFIGURATION_TRACING_DISABLED,
ENV_KEY,
FEATURE_FILTER_TYPE_KEY,
FF_MAX_VARIANTS_KEY,
FF_FEATURES_KEY,
HOST_TYPE_KEY,
HostType,
KEY_VAULT_CONFIGURED_TAG,
Expand All @@ -28,17 +32,18 @@ export function listConfigurationSettingsWithTrace(
requestTracingEnabled: boolean;
initialLoadCompleted: boolean;
appConfigOptions: AzureAppConfigurationOptions | undefined;
featureFlagTracingOptions: FeatureFlagTracingOptions | undefined;
},
client: AppConfigurationClient,
listOptions: ListConfigurationSettingsOptions
) {
const { requestTracingEnabled, initialLoadCompleted, appConfigOptions } = requestTracingOptions;
const { requestTracingEnabled, initialLoadCompleted, appConfigOptions, featureFlagTracingOptions } = requestTracingOptions;

const actualListOptions = { ...listOptions };
if (requestTracingEnabled) {
actualListOptions.requestOptions = {
customHeaders: {
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(appConfigOptions, initialLoadCompleted)
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(appConfigOptions, featureFlagTracingOptions, initialLoadCompleted)
}
};
}
Expand All @@ -51,26 +56,27 @@ export function getConfigurationSettingWithTrace(
requestTracingEnabled: boolean;
initialLoadCompleted: boolean;
appConfigOptions: AzureAppConfigurationOptions | undefined;
featureFlagTracingOptions: FeatureFlagTracingOptions | undefined;
},
client: AppConfigurationClient,
configurationSettingId: ConfigurationSettingId,
getOptions?: GetConfigurationSettingOptions,
) {
const { requestTracingEnabled, initialLoadCompleted, appConfigOptions } = requestTracingOptions;
const { requestTracingEnabled, initialLoadCompleted, appConfigOptions, featureFlagTracingOptions } = requestTracingOptions;
const actualGetOptions = { ...getOptions };

if (requestTracingEnabled) {
actualGetOptions.requestOptions = {
customHeaders: {
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(appConfigOptions, initialLoadCompleted)
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(appConfigOptions, featureFlagTracingOptions, initialLoadCompleted)
}
};
}

return client.getConfigurationSetting(configurationSettingId, actualGetOptions);
}

export function createCorrelationContextHeader(options: AzureAppConfigurationOptions | undefined, isInitialLoadCompleted: boolean): string {
export function createCorrelationContextHeader(options: AzureAppConfigurationOptions | undefined, featureFlagTracing: FeatureFlagTracingOptions | undefined, isInitialLoadCompleted: boolean): string {
/*
RequestType: 'Startup' during application starting up, 'Watch' after startup completed.
Host: identify with defined envs
Expand All @@ -82,6 +88,14 @@ export function createCorrelationContextHeader(options: AzureAppConfigurationOpt
keyValues.set(HOST_TYPE_KEY, getHostType());
keyValues.set(ENV_KEY, isDevEnvironment() ? DEV_ENV_VAL : undefined);

if (featureFlagTracing) {
keyValues.set(FEATURE_FILTER_TYPE_KEY, featureFlagTracing.usesAnyFeatureFilter() ? featureFlagTracing.createFeatureFiltersString() : undefined);
keyValues.set(FF_FEATURES_KEY, featureFlagTracing.usesAnyTracingFeature() ? featureFlagTracing.createFeaturesString() : undefined);
if (featureFlagTracing.maxVariants > 0) {
keyValues.set(FF_MAX_VARIANTS_KEY, featureFlagTracing.maxVariants.toString());
}
}

const tags: string[] = [];
if (options?.keyVaultOptions) {
const { credential, secretClients, secretResolver } = options.keyVaultOptions;
Expand Down
Loading
Loading