From a5b5bb938ae27462b77282401c4b815e405bd27a Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Wed, 16 Apr 2025 18:20:57 +0800 Subject: [PATCH 1/7] wip --- src/AzureAppConfigurationImpl.ts | 45 ++++++++++++++----- .../AIConfigurationTracingOptions.ts | 12 +++++ .../FeatureFlagTracingOptions.ts | 2 +- src/requestTracing/constants.ts | 7 +++ src/requestTracing/utils.ts | 2 + 5 files changed, 55 insertions(+), 13 deletions(-) create mode 100644 src/requestTracing/AIConfigurationTracingOptions.ts diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index d6565d84..9f8f8456 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -24,11 +24,12 @@ import { CONDITIONS_KEY_NAME, CLIENT_FILTERS_KEY_NAME } from "./featureManagement/constants.js"; -import { FM_PACKAGE_NAME } from "./requestTracing/constants.js"; +import { FM_PACKAGE_NAME, AI_MIME_PROFILE, AI_CHAT_COMPLETION_MIME_PROFILE } from "./requestTracing/constants.js"; import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js"; import { RefreshTimer } from "./refresh/RefreshTimer.js"; import { RequestTracingOptions, getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js"; import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOptions.js"; +import { AIConfigurationTracingOptions } from "./requestTracing/AIConfigurationTracingOptions.js"; import { KeyFilter, LabelFilter, SettingSelector } from "./types.js"; import { ConfigurationClientManager } from "./ConfigurationClientManager.js"; @@ -58,6 +59,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { #isFailoverRequest: boolean = false; #featureFlagTracing: FeatureFlagTracingOptions | undefined; #fmVersion: string | undefined; + #aiConfigurationTracing: AIConfigurationTracingOptions | undefined; // Refresh #refreshInProgress: boolean = false; @@ -178,7 +180,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { replicaCount: this.#clientManager.getReplicaCount(), isFailoverRequest: this.#isFailoverRequest, featureFlagTracing: this.#featureFlagTracing, - fmVersion: this.#fmVersion + fmVersion: this.#fmVersion, + aiConfigurationTracing: this.#aiConfigurationTracing }; } @@ -634,11 +637,22 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } async #processKeyValues(setting: ConfigurationSetting): Promise<[string, unknown]> { + this.#setAIConfigurationTracing(setting); + const [key, value] = await this.#processAdapters(setting); const trimmedKey = this.#keyWithPrefixesTrimmed(key); return [trimmedKey, value]; } + #setAIConfigurationTracing(setting: ConfigurationSetting): void { + if (this.#requestTracingEnabled && this.#aiConfigurationTracing !== undefined) { + // Reset old AI configuration tracing in order to track the information present in the current response from server. + this.#aiConfigurationTracing.reset(); + + + } + } + async #processAdapters(setting: ConfigurationSetting): Promise<[string, unknown]> { for (const adapter of this.#adapters) { if (adapter.canProcess(setting)) { @@ -675,7 +689,24 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { }; } + this.#setFeatureFlagTracing(featureFlag); + + return featureFlag; + } + + #createFeatureFlagReference(setting: ConfigurationSetting): string { + let featureFlagReference = `${this.#clientManager.endpoint.origin}/kv/${setting.key}`; + if (setting.label && setting.label.trim().length !== 0) { + featureFlagReference += `?label=${setting.label}`; + } + return featureFlagReference; + } + + #setFeatureFlagTracing(featureFlag: any): void { if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) { + // Reset old feature flag tracing in order to track the information present in the current response from server. + this.#featureFlagTracing.reset(); + if (featureFlag[CONDITIONS_KEY_NAME] && featureFlag[CONDITIONS_KEY_NAME][CLIENT_FILTERS_KEY_NAME] && Array.isArray(featureFlag[CONDITIONS_KEY_NAME][CLIENT_FILTERS_KEY_NAME])) { @@ -693,16 +724,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { this.#featureFlagTracing.usesSeed = true; } } - - return featureFlag; - } - - #createFeatureFlagReference(setting: ConfigurationSetting): string { - let featureFlagReference = `${this.#clientManager.endpoint.origin}/kv/${setting.key}`; - if (setting.label && setting.label.trim().length !== 0) { - featureFlagReference += `?label=${setting.label}`; - } - return featureFlagReference; } } diff --git a/src/requestTracing/AIConfigurationTracingOptions.ts b/src/requestTracing/AIConfigurationTracingOptions.ts new file mode 100644 index 00000000..97dad924 --- /dev/null +++ b/src/requestTracing/AIConfigurationTracingOptions.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export class AIConfigurationTracingOptions { + usesAIConfiguration: boolean = false; + usesAIChatCompletionConfiguration: boolean = false; + + reset(): void { + this.usesAIConfiguration = false; + this.usesAIChatCompletionConfiguration = false; + } +} diff --git a/src/requestTracing/FeatureFlagTracingOptions.ts b/src/requestTracing/FeatureFlagTracingOptions.ts index 006d969e..e47d8887 100644 --- a/src/requestTracing/FeatureFlagTracingOptions.ts +++ b/src/requestTracing/FeatureFlagTracingOptions.ts @@ -18,7 +18,7 @@ export class FeatureFlagTracingOptions { usesSeed: boolean = false; maxVariants: number = 0; - resetFeatureFlagTracing(): void { + reset(): void { this.usesCustomFilter = false; this.usesTimeWindowFilter = false; this.usesTargetingFilter = false; diff --git a/src/requestTracing/constants.ts b/src/requestTracing/constants.ts index 74ca58bb..6d1f3f3f 100644 --- a/src/requestTracing/constants.ts +++ b/src/requestTracing/constants.ts @@ -70,4 +70,11 @@ export const FF_MAX_VARIANTS_KEY = "MaxVariants"; export const FF_SEED_USED_TAG = "Seed"; export const FF_FEATURES_KEY = "FFFeatures"; +// AI Configuration tracing +export const AI_CONFIGURATION_TAG = "AI"; +export const AI_CHAT_COMPLETION_CONFIGURATION_TAG = "AICC"; + +export const AI_MIME_PROFILE = "https://azconfig.io/mime-profiles/ai"; +export const AI_CHAT_COMPLETION_MIME_PROFILE = "https://azconfig.io/mime-profiles/ai/chat-completion"; + export const DELIMITER = "+"; diff --git a/src/requestTracing/utils.ts b/src/requestTracing/utils.ts index 2e8b1124..8532e07a 100644 --- a/src/requestTracing/utils.ts +++ b/src/requestTracing/utils.ts @@ -4,6 +4,7 @@ import { AppConfigurationClient, ConfigurationSettingId, GetConfigurationSettingOptions, ListConfigurationSettingsOptions } from "@azure/app-configuration"; import { AzureAppConfigurationOptions } from "../AzureAppConfigurationOptions.js"; import { FeatureFlagTracingOptions } from "./FeatureFlagTracingOptions.js"; +import { AIConfigurationTracingOptions } from "./AIConfigurationTracingOptions.js"; import { AZURE_FUNCTION_ENV_VAR, AZURE_WEB_APP_ENV_VAR, @@ -39,6 +40,7 @@ export interface RequestTracingOptions { isFailoverRequest: boolean; featureFlagTracing: FeatureFlagTracingOptions | undefined; fmVersion: string | undefined; + aiConfigurationTracing: AIConfigurationTracingOptions | undefined; } // Utils From 76579dddeff1e7beb6d5275735e97f25636f9dbf Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 17 Apr 2025 14:47:26 +0800 Subject: [PATCH 2/7] add request tracing for ai configuration usage --- package.json | 2 +- src/AzureAppConfigurationImpl.ts | 36 +++++++++++---- src/JsonKeyValueAdapter.ts | 25 ++--------- src/common/contentType.ts | 44 +++++++++++++++++++ .../AIConfigurationTracingOptions.ts | 4 ++ src/requestTracing/utils.ts | 24 ++++++++-- test/requestTracing.test.ts | 44 +++++++++++++++++++ 7 files changed, 143 insertions(+), 36 deletions(-) create mode 100644 src/common/contentType.ts diff --git a/package.json b/package.json index 7c757350..2885965e 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "dev": "rollup --config --watch", "lint": "eslint src/ test/", "fix-lint": "eslint src/ test/ --fix", - "test": "mocha out/test/*.test.{js,cjs,mjs} --parallel" + "test": "mocha out/test/requestTracing.test.{js,cjs,mjs} --parallel" }, "repository": { "type": "git", diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 9f8f8456..8c2503ec 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -25,6 +25,7 @@ import { CLIENT_FILTERS_KEY_NAME } from "./featureManagement/constants.js"; import { FM_PACKAGE_NAME, AI_MIME_PROFILE, AI_CHAT_COMPLETION_MIME_PROFILE } from "./requestTracing/constants.js"; +import { parseContentType, isJsonContentType } from "./common/contentType.js"; import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js"; import { RefreshTimer } from "./refresh/RefreshTimer.js"; import { RequestTracingOptions, getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js"; @@ -99,6 +100,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { // enable request tracing if not opt-out this.#requestTracingEnabled = requestTracingEnabled(); if (this.#requestTracingEnabled) { + this.#aiConfigurationTracing = new AIConfigurationTracingOptions(); this.#featureFlagTracing = new FeatureFlagTracingOptions(); } @@ -419,9 +421,14 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { await this.#updateWatchedKeyValuesEtag(loadedSettings); } + if (this.#requestTracingEnabled && this.#aiConfigurationTracing !== undefined) { + // Reset old AI configuration tracing in order to track the information present in the current response from server. + this.#aiConfigurationTracing.reset(); + } + // process key-values, watched settings have higher priority for (const setting of loadedSettings) { - const [key, value] = await this.#processKeyValues(setting); + const [key, value] = await this.#processKeyValue(setting); keyValues.push([key, value]); } @@ -470,6 +477,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { const loadFeatureFlag = true; const featureFlagSettings = await this.#loadConfigurationSettings(loadFeatureFlag); + if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) { + // Reset old feature flag tracing in order to track the information present in the current response from server. + this.#featureFlagTracing.reset(); + } + // parse feature flags const featureFlags = await Promise.all( featureFlagSettings.map(setting => this.#parseFeatureFlag(setting)) @@ -636,7 +648,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { throw new Error("All clients failed to get configuration settings."); } - async #processKeyValues(setting: ConfigurationSetting): Promise<[string, unknown]> { + async #processKeyValue(setting: ConfigurationSetting): Promise<[string, unknown]> { this.#setAIConfigurationTracing(setting); const [key, value] = await this.#processAdapters(setting); @@ -646,10 +658,19 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { #setAIConfigurationTracing(setting: ConfigurationSetting): void { if (this.#requestTracingEnabled && this.#aiConfigurationTracing !== undefined) { - // Reset old AI configuration tracing in order to track the information present in the current response from server. - this.#aiConfigurationTracing.reset(); - - + const contentType = parseContentType(setting.contentType); + if (isJsonContentType(contentType)) { + const profile = contentType?.parameters["profile"]; + if (profile === undefined) { + return; + } + if (profile.includes(AI_MIME_PROFILE)) { + this.#aiConfigurationTracing.usesAIConfiguration = true; + } + if (profile.includes(AI_CHAT_COMPLETION_MIME_PROFILE)) { + this.#aiConfigurationTracing.usesAIChatCompletionConfiguration = true; + } + } } } @@ -704,9 +725,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { #setFeatureFlagTracing(featureFlag: any): void { if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) { - // Reset old feature flag tracing in order to track the information present in the current response from server. - this.#featureFlagTracing.reset(); - if (featureFlag[CONDITIONS_KEY_NAME] && featureFlag[CONDITIONS_KEY_NAME][CLIENT_FILTERS_KEY_NAME] && Array.isArray(featureFlag[CONDITIONS_KEY_NAME][CLIENT_FILTERS_KEY_NAME])) { diff --git a/src/JsonKeyValueAdapter.ts b/src/JsonKeyValueAdapter.ts index d9157a45..dcce1033 100644 --- a/src/JsonKeyValueAdapter.ts +++ b/src/JsonKeyValueAdapter.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. import { ConfigurationSetting, featureFlagContentType, secretReferenceContentType } from "@azure/app-configuration"; +import { parseContentType, isJsonContentType } from "./common/contentType.js"; import { IKeyValueAdapter } from "./IKeyValueAdapter.js"; export class JsonKeyValueAdapter implements IKeyValueAdapter { @@ -17,7 +18,8 @@ export class JsonKeyValueAdapter implements IKeyValueAdapter { if (JsonKeyValueAdapter.#ExcludedJsonContentTypes.includes(setting.contentType)) { return false; } - return isJsonContentType(setting.contentType); + const contentType = parseContentType(setting.contentType); + return isJsonContentType(contentType); } async processKeyValue(setting: ConfigurationSetting): Promise<[string, unknown]> { @@ -34,24 +36,3 @@ export class JsonKeyValueAdapter implements IKeyValueAdapter { return [setting.key, parsedValue]; } } - -// Determine whether a content type string is a valid JSON content type. -// https://docs.microsoft.com/en-us/azure/azure-app-configuration/howto-leverage-json-content-type -function isJsonContentType(contentTypeValue: string): boolean { - if (!contentTypeValue) { - return false; - } - - const contentTypeNormalized: string = contentTypeValue.trim().toLowerCase(); - const mimeType: string = contentTypeNormalized.split(";", 1)[0].trim(); - const typeParts: string[] = mimeType.split("/"); - if (typeParts.length !== 2) { - return false; - } - - if (typeParts[0] !== "application") { - return false; - } - - return typeParts[1].split("+").includes("json"); -} diff --git a/src/common/contentType.ts b/src/common/contentType.ts new file mode 100644 index 00000000..cb722636 --- /dev/null +++ b/src/common/contentType.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export type ContentType = { + mediaType: string; + parameters: Record; +} + +export function parseContentType(contentTypeValue: string | undefined): ContentType | undefined { + if (!contentTypeValue) { + return undefined; + } + const [mediaType, ...args] = contentTypeValue.split(";").map((s) => s.trim()); + const parameters: Record = {}; + + for (const param of args) { + const [key, value] = param.split("=").map((s) => s.trim()); + if (key && value) { + parameters[key] = value; + } + } + + return { mediaType, parameters }; +} + +// Determine whether a content type string is a valid JSON content type. +// https://docs.microsoft.com/en-us/azure/azure-app-configuration/howto-leverage-json-content-type +export function isJsonContentType(contentType: ContentType | undefined): boolean { + const mediaType = contentType?.mediaType?.trim().toLowerCase(); + if (!mediaType) { + return false; + } + + const typeParts: string[] = mediaType.split("/"); + if (typeParts.length !== 2) { + return false; + } + + if (typeParts[0] !== "application") { + return false; + } + + return typeParts[1].split("+").includes("json"); +} diff --git a/src/requestTracing/AIConfigurationTracingOptions.ts b/src/requestTracing/AIConfigurationTracingOptions.ts index 97dad924..1c7671ae 100644 --- a/src/requestTracing/AIConfigurationTracingOptions.ts +++ b/src/requestTracing/AIConfigurationTracingOptions.ts @@ -9,4 +9,8 @@ export class AIConfigurationTracingOptions { this.usesAIConfiguration = false; this.usesAIChatCompletionConfiguration = false; } + + usesAnyTracingFeature() { + return this.usesAIConfiguration || this.usesAIChatCompletionConfiguration; + } } diff --git a/src/requestTracing/utils.ts b/src/requestTracing/utils.ts index 8532e07a..5039fe8d 100644 --- a/src/requestTracing/utils.ts +++ b/src/requestTracing/utils.ts @@ -29,7 +29,10 @@ import { FAILOVER_REQUEST_TAG, FEATURES_KEY, LOAD_BALANCE_CONFIGURED_TAG, - FM_VERSION_KEY + FM_VERSION_KEY, + DELIMITER, + AI_CONFIGURATION_TAG, + AI_CHAT_COMPLETION_CONFIGURATION_TAG } from "./constants"; export interface RequestTracingOptions { @@ -127,9 +130,22 @@ export function createCorrelationContextHeader(requestTracingOptions: RequestTra keyValues.set(FM_VERSION_KEY, requestTracingOptions.fmVersion); } - // Compact tags: Features=LB+... - if (appConfigOptions?.loadBalancingEnabled) { - keyValues.set(FEATURES_KEY, LOAD_BALANCE_CONFIGURED_TAG); + // Compact tags: Features=LB+AI+AICC... + if (appConfigOptions?.loadBalancingEnabled || requestTracingOptions.aiConfigurationTracing?.usesAnyTracingFeature()) { + const tags: string[] = []; + if (appConfigOptions?.loadBalancingEnabled) { + tags.push(LOAD_BALANCE_CONFIGURED_TAG); + } + if (requestTracingOptions.aiConfigurationTracing?.usesAIConfiguration) { + tags.push(AI_CONFIGURATION_TAG); + } + if (requestTracingOptions.aiConfigurationTracing?.usesAIChatCompletionConfiguration) { + tags.push(AI_CHAT_COMPLETION_CONFIGURATION_TAG); + } + + if (tags.length > 0) { + keyValues.set(FEATURES_KEY, tags.join(DELIMITER)); + } } const contextParts: string[] = []; diff --git a/test/requestTracing.test.ts b/test/requestTracing.test.ts index ed3fdaee..3179602a 100644 --- a/test/requestTracing.test.ts +++ b/test/requestTracing.test.ts @@ -64,6 +64,19 @@ describe("request tracing", function () { expect(correlationContext.includes("UsesKeyVault")).eq(true); }); + it("should have loadbalancing tag in correlation-context header", async () => { + try { + await load(createMockedConnectionString(fakeEndpoint), { + clientOptions, + loadBalancingEnabled: true, + }); + } catch (e) { /* empty */ } + expect(headerPolicy.headers).not.undefined; + const correlationContext = headerPolicy.headers.get("Correlation-Context"); + expect(correlationContext).not.undefined; + expect(correlationContext.includes("Features=LB")).eq(true); + }); + it("should have replica count in correlation-context header", async () => { const replicaCount = 2; sinon.stub(ConfigurationClientManager.prototype, "getReplicaCount").returns(replicaCount); @@ -293,6 +306,37 @@ describe("request tracing", function () { restoreMocks(); }); + it("should have AI tag in correlation-context header if key values use AI configuration", async () => { + let correlationContext: string = ""; + const listKvCallback = (listOptions) => { + correlationContext = listOptions?.requestOptions?.customHeaders[CORRELATION_CONTEXT_HEADER_NAME] ?? ""; + }; + + mockAppConfigurationClientListConfigurationSettings([[ + createMockedKeyValue({ contentType: "application/json; profile=\"https://azconfig.io/mime-profiles/ai/chat-completion\"" }) + ]], listKvCallback); + + const settings = await load(createMockedConnectionString(fakeEndpoint), { + refreshOptions: { + enabled: true, + refreshIntervalInMs: 1000 + } + }); + + expect(correlationContext).not.undefined; + expect(correlationContext?.includes("RequestType=Startup")).eq(true); + + await sleepInMs(1000 + 1); + try { + await settings.refresh(); + } catch (e) { /* empty */ } + expect(headerPolicy.headers).not.undefined; + expect(correlationContext).not.undefined; + expect(correlationContext?.includes("Features=AI+AICC")).eq(true); + + restoreMocks(); + }); + describe("request tracing in Web Worker environment", () => { let originalNavigator; let originalWorkerNavigator; From d8b8b8f2a3d5609f4d21d940ff609b1d3d64b2ee Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 17 Apr 2025 14:53:27 +0800 Subject: [PATCH 3/7] revert script change --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2885965e..7c757350 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "dev": "rollup --config --watch", "lint": "eslint src/ test/", "fix-lint": "eslint src/ test/ --fix", - "test": "mocha out/test/requestTracing.test.{js,cjs,mjs} --parallel" + "test": "mocha out/test/*.test.{js,cjs,mjs} --parallel" }, "repository": { "type": "git", From e6d23d6025b7a630923127884f666f482b7cf6c5 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 17 Apr 2025 14:59:12 +0800 Subject: [PATCH 4/7] update --- src/common/contentType.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/contentType.ts b/src/common/contentType.ts index cb722636..eebfc57a 100644 --- a/src/common/contentType.ts +++ b/src/common/contentType.ts @@ -10,11 +10,11 @@ export function parseContentType(contentTypeValue: string | undefined): ContentT if (!contentTypeValue) { return undefined; } - const [mediaType, ...args] = contentTypeValue.split(";").map((s) => s.trim()); + const [mediaType, ...args] = contentTypeValue.split(";").map((s) => s.trim().toLowerCase()); const parameters: Record = {}; for (const param of args) { - const [key, value] = param.split("=").map((s) => s.trim()); + const [key, value] = param.split("=").map((s) => s.trim().toLowerCase()); if (key && value) { parameters[key] = value; } @@ -26,7 +26,7 @@ export function parseContentType(contentTypeValue: string | undefined): ContentT // Determine whether a content type string is a valid JSON content type. // https://docs.microsoft.com/en-us/azure/azure-app-configuration/howto-leverage-json-content-type export function isJsonContentType(contentType: ContentType | undefined): boolean { - const mediaType = contentType?.mediaType?.trim().toLowerCase(); + const mediaType = contentType?.mediaType; if (!mediaType) { return false; } From 4bc26de2df8ab2857c4346501ed35cfd97747da5 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 17 Apr 2025 15:08:14 +0800 Subject: [PATCH 5/7] add comment --- src/AzureAppConfigurationImpl.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 8c2503ec..8f5c4eba 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -659,6 +659,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { #setAIConfigurationTracing(setting: ConfigurationSetting): void { if (this.#requestTracingEnabled && this.#aiConfigurationTracing !== undefined) { const contentType = parseContentType(setting.contentType); + // content type: application/json; profile="https://azconfig.io/mime-profiles/ai" if (isJsonContentType(contentType)) { const profile = contentType?.parameters["profile"]; if (profile === undefined) { From 5225630f95ea48780cf8c68d9c570de615f13f0f Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 18 Apr 2025 01:03:31 +0800 Subject: [PATCH 6/7] update --- .../FeatureFlagTracingOptions.ts | 35 ++++----------- src/requestTracing/utils.ts | 44 ++++++++++--------- 2 files changed, 33 insertions(+), 46 deletions(-) diff --git a/src/requestTracing/FeatureFlagTracingOptions.ts b/src/requestTracing/FeatureFlagTracingOptions.ts index e47d8887..dd1781db 100644 --- a/src/requestTracing/FeatureFlagTracingOptions.ts +++ b/src/requestTracing/FeatureFlagTracingOptions.ts @@ -52,44 +52,27 @@ export class FeatureFlagTracingOptions { } createFeatureFiltersString(): string { - if (!this.usesAnyFeatureFilter()) { - return ""; - } - - let result: string = ""; + const tags: string[] = []; if (this.usesCustomFilter) { - result += CUSTOM_FILTER_KEY; + tags.push(CUSTOM_FILTER_KEY); } if (this.usesTimeWindowFilter) { - if (result !== "") { - result += DELIMITER; - } - result += TIME_WINDOW_FILTER_KEY; + tags.push(TIME_WINDOW_FILTER_KEY); } if (this.usesTargetingFilter) { - if (result !== "") { - result += DELIMITER; - } - result += TARGETING_FILTER_KEY; + tags.push(TARGETING_FILTER_KEY); } - return result; + return tags.join(DELIMITER); } createFeaturesString(): string { - if (!this.usesAnyTracingFeature()) { - return ""; - } - - let result: string = ""; + const tags: string[] = []; if (this.usesSeed) { - result += FF_SEED_USED_TAG; + tags.push(FF_SEED_USED_TAG); } if (this.usesTelemetry) { - if (result !== "") { - result += DELIMITER; - } - result += FF_TELEMETRY_USED_TAG; + tags.push(FF_TELEMETRY_USED_TAG); } - return result; + return tags.join(DELIMITER); } } diff --git a/src/requestTracing/utils.ts b/src/requestTracing/utils.ts index 5039fe8d..9332c656 100644 --- a/src/requestTracing/utils.ts +++ b/src/requestTracing/utils.ts @@ -130,28 +130,13 @@ export function createCorrelationContextHeader(requestTracingOptions: RequestTra keyValues.set(FM_VERSION_KEY, requestTracingOptions.fmVersion); } - // Compact tags: Features=LB+AI+AICC... - if (appConfigOptions?.loadBalancingEnabled || requestTracingOptions.aiConfigurationTracing?.usesAnyTracingFeature()) { - const tags: string[] = []; - if (appConfigOptions?.loadBalancingEnabled) { - tags.push(LOAD_BALANCE_CONFIGURED_TAG); - } - if (requestTracingOptions.aiConfigurationTracing?.usesAIConfiguration) { - tags.push(AI_CONFIGURATION_TAG); - } - if (requestTracingOptions.aiConfigurationTracing?.usesAIChatCompletionConfiguration) { - tags.push(AI_CHAT_COMPLETION_CONFIGURATION_TAG); - } - - if (tags.length > 0) { - keyValues.set(FEATURES_KEY, tags.join(DELIMITER)); - } - } + // Use compact tags for new tracing features: Features=LB+AI+AICC... + keyValues.set(FEATURES_KEY, usesAnyTracingFeature(requestTracingOptions) ? createFeaturesString(requestTracingOptions) : undefined); const contextParts: string[] = []; - for (const [k, v] of keyValues) { - if (v !== undefined) { - contextParts.push(`${k}=${v}`); + for (const [key, value] of keyValues) { + if (value !== undefined) { + contextParts.push(`${key}=${value}`); } } for (const tag of tags) { @@ -167,6 +152,25 @@ export function requestTracingEnabled(): boolean { return !disabled; } +function usesAnyTracingFeature(requestTracingOptions: RequestTracingOptions): boolean { + return (requestTracingOptions.appConfigOptions?.loadBalancingEnabled ?? false) || + (requestTracingOptions.aiConfigurationTracing?.usesAnyTracingFeature() ?? false); +} + +function createFeaturesString(requestTracingOptions: RequestTracingOptions): string { + const tags: string[] = []; + if (requestTracingOptions.appConfigOptions?.loadBalancingEnabled) { + tags.push(LOAD_BALANCE_CONFIGURED_TAG); + } + if (requestTracingOptions.aiConfigurationTracing?.usesAIConfiguration) { + tags.push(AI_CONFIGURATION_TAG); + } + if (requestTracingOptions.aiConfigurationTracing?.usesAIChatCompletionConfiguration) { + tags.push(AI_CHAT_COMPLETION_CONFIGURATION_TAG); + } + return tags.join(DELIMITER); +} + function getEnvironmentVariable(name: string) { // Make it compatible with non-Node.js runtime if (typeof process !== "undefined" && typeof process?.env === "object") { From 6fffe1bd414756b3bb2bea1edea99f818736ddc1 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 18 Apr 2025 12:49:54 +0800 Subject: [PATCH 7/7] exclude ff & secret reference --- src/AzureAppConfigurationImpl.ts | 8 +++++--- src/common/contentType.ts | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 8f5c4eba..35fc26ee 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -25,7 +25,7 @@ import { CLIENT_FILTERS_KEY_NAME } from "./featureManagement/constants.js"; import { FM_PACKAGE_NAME, AI_MIME_PROFILE, AI_CHAT_COMPLETION_MIME_PROFILE } from "./requestTracing/constants.js"; -import { parseContentType, isJsonContentType } from "./common/contentType.js"; +import { parseContentType, isJsonContentType, isFeatureFlagContentType, isSecretReferenceContentType } from "./common/contentType.js"; import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js"; import { RefreshTimer } from "./refresh/RefreshTimer.js"; import { RequestTracingOptions, getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js"; @@ -659,8 +659,10 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { #setAIConfigurationTracing(setting: ConfigurationSetting): void { if (this.#requestTracingEnabled && this.#aiConfigurationTracing !== undefined) { const contentType = parseContentType(setting.contentType); - // content type: application/json; profile="https://azconfig.io/mime-profiles/ai" - if (isJsonContentType(contentType)) { + // content type: "application/json; profile=\"https://azconfig.io/mime-profiles/ai\""" + if (isJsonContentType(contentType) && + !isFeatureFlagContentType(contentType) && + !isSecretReferenceContentType(contentType)) { const profile = contentType?.parameters["profile"]; if (profile === undefined) { return; diff --git a/src/common/contentType.ts b/src/common/contentType.ts index eebfc57a..4891f425 100644 --- a/src/common/contentType.ts +++ b/src/common/contentType.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +import { secretReferenceContentType, featureFlagContentType } from "@azure/app-configuration"; + export type ContentType = { mediaType: string; parameters: Record; @@ -42,3 +44,19 @@ export function isJsonContentType(contentType: ContentType | undefined): boolean return typeParts[1].split("+").includes("json"); } + +export function isFeatureFlagContentType(contentType: ContentType | undefined): boolean { + const mediaType = contentType?.mediaType; + if (!mediaType) { + return false; + } + return mediaType === featureFlagContentType; +} + +export function isSecretReferenceContentType(contentType: ContentType | undefined): boolean { + const mediaType = contentType?.mediaType; + if (!mediaType) { + return false; + } + return mediaType === secretReferenceContentType; +}