Skip to content

Commit 411d083

Browse files
Merge pull request #185 from Azure/zhiyuanliang/contenttype-tracing
AI Configuration request tracing
1 parent 86d3a09 commit 411d083

File tree

8 files changed

+228
-71
lines changed

8 files changed

+228
-71
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ import {
2424
CONDITIONS_KEY_NAME,
2525
CLIENT_FILTERS_KEY_NAME
2626
} from "./featureManagement/constants.js";
27-
import { FM_PACKAGE_NAME } from "./requestTracing/constants.js";
27+
import { FM_PACKAGE_NAME, AI_MIME_PROFILE, AI_CHAT_COMPLETION_MIME_PROFILE } from "./requestTracing/constants.js";
28+
import { parseContentType, isJsonContentType, isFeatureFlagContentType, isSecretReferenceContentType } from "./common/contentType.js";
2829
import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js";
2930
import { RefreshTimer } from "./refresh/RefreshTimer.js";
3031
import { RequestTracingOptions, getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js";
3132
import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOptions.js";
33+
import { AIConfigurationTracingOptions } from "./requestTracing/AIConfigurationTracingOptions.js";
3234
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";
3335
import { ConfigurationClientManager } from "./ConfigurationClientManager.js";
3436

@@ -58,6 +60,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
5860
#isFailoverRequest: boolean = false;
5961
#featureFlagTracing: FeatureFlagTracingOptions | undefined;
6062
#fmVersion: string | undefined;
63+
#aiConfigurationTracing: AIConfigurationTracingOptions | undefined;
6164

6265
// Refresh
6366
#refreshInProgress: boolean = false;
@@ -97,6 +100,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
97100
// enable request tracing if not opt-out
98101
this.#requestTracingEnabled = requestTracingEnabled();
99102
if (this.#requestTracingEnabled) {
103+
this.#aiConfigurationTracing = new AIConfigurationTracingOptions();
100104
this.#featureFlagTracing = new FeatureFlagTracingOptions();
101105
}
102106

@@ -178,7 +182,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
178182
replicaCount: this.#clientManager.getReplicaCount(),
179183
isFailoverRequest: this.#isFailoverRequest,
180184
featureFlagTracing: this.#featureFlagTracing,
181-
fmVersion: this.#fmVersion
185+
fmVersion: this.#fmVersion,
186+
aiConfigurationTracing: this.#aiConfigurationTracing
182187
};
183188
}
184189

@@ -416,9 +421,14 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
416421
await this.#updateWatchedKeyValuesEtag(loadedSettings);
417422
}
418423

424+
if (this.#requestTracingEnabled && this.#aiConfigurationTracing !== undefined) {
425+
// Reset old AI configuration tracing in order to track the information present in the current response from server.
426+
this.#aiConfigurationTracing.reset();
427+
}
428+
419429
// process key-values, watched settings have higher priority
420430
for (const setting of loadedSettings) {
421-
const [key, value] = await this.#processKeyValues(setting);
431+
const [key, value] = await this.#processKeyValue(setting);
422432
keyValues.push([key, value]);
423433
}
424434

@@ -467,6 +477,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
467477
const loadFeatureFlag = true;
468478
const featureFlagSettings = await this.#loadConfigurationSettings(loadFeatureFlag);
469479

480+
if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) {
481+
// Reset old feature flag tracing in order to track the information present in the current response from server.
482+
this.#featureFlagTracing.reset();
483+
}
484+
470485
// parse feature flags
471486
const featureFlags = await Promise.all(
472487
featureFlagSettings.map(setting => this.#parseFeatureFlag(setting))
@@ -633,12 +648,35 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
633648
throw new Error("All clients failed to get configuration settings.");
634649
}
635650

636-
async #processKeyValues(setting: ConfigurationSetting<string>): Promise<[string, unknown]> {
651+
async #processKeyValue(setting: ConfigurationSetting<string>): Promise<[string, unknown]> {
652+
this.#setAIConfigurationTracing(setting);
653+
637654
const [key, value] = await this.#processAdapters(setting);
638655
const trimmedKey = this.#keyWithPrefixesTrimmed(key);
639656
return [trimmedKey, value];
640657
}
641658

659+
#setAIConfigurationTracing(setting: ConfigurationSetting<string>): void {
660+
if (this.#requestTracingEnabled && this.#aiConfigurationTracing !== undefined) {
661+
const contentType = parseContentType(setting.contentType);
662+
// content type: "application/json; profile=\"https://azconfig.io/mime-profiles/ai\"""
663+
if (isJsonContentType(contentType) &&
664+
!isFeatureFlagContentType(contentType) &&
665+
!isSecretReferenceContentType(contentType)) {
666+
const profile = contentType?.parameters["profile"];
667+
if (profile === undefined) {
668+
return;
669+
}
670+
if (profile.includes(AI_MIME_PROFILE)) {
671+
this.#aiConfigurationTracing.usesAIConfiguration = true;
672+
}
673+
if (profile.includes(AI_CHAT_COMPLETION_MIME_PROFILE)) {
674+
this.#aiConfigurationTracing.usesAIChatCompletionConfiguration = true;
675+
}
676+
}
677+
}
678+
}
679+
642680
async #processAdapters(setting: ConfigurationSetting<string>): Promise<[string, unknown]> {
643681
for (const adapter of this.#adapters) {
644682
if (adapter.canProcess(setting)) {
@@ -675,6 +713,20 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
675713
};
676714
}
677715

716+
this.#setFeatureFlagTracing(featureFlag);
717+
718+
return featureFlag;
719+
}
720+
721+
#createFeatureFlagReference(setting: ConfigurationSetting<string>): string {
722+
let featureFlagReference = `${this.#clientManager.endpoint.origin}/kv/${setting.key}`;
723+
if (setting.label && setting.label.trim().length !== 0) {
724+
featureFlagReference += `?label=${setting.label}`;
725+
}
726+
return featureFlagReference;
727+
}
728+
729+
#setFeatureFlagTracing(featureFlag: any): void {
678730
if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) {
679731
if (featureFlag[CONDITIONS_KEY_NAME] &&
680732
featureFlag[CONDITIONS_KEY_NAME][CLIENT_FILTERS_KEY_NAME] &&
@@ -693,16 +745,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
693745
this.#featureFlagTracing.usesSeed = true;
694746
}
695747
}
696-
697-
return featureFlag;
698-
}
699-
700-
#createFeatureFlagReference(setting: ConfigurationSetting<string>): string {
701-
let featureFlagReference = `${this.#clientManager.endpoint.origin}/kv/${setting.key}`;
702-
if (setting.label && setting.label.trim().length !== 0) {
703-
featureFlagReference += `?label=${setting.label}`;
704-
}
705-
return featureFlagReference;
706748
}
707749
}
708750

src/JsonKeyValueAdapter.ts

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license.
33

44
import { ConfigurationSetting, featureFlagContentType, secretReferenceContentType } from "@azure/app-configuration";
5+
import { parseContentType, isJsonContentType } from "./common/contentType.js";
56
import { IKeyValueAdapter } from "./IKeyValueAdapter.js";
67

78
export class JsonKeyValueAdapter implements IKeyValueAdapter {
@@ -17,7 +18,8 @@ export class JsonKeyValueAdapter implements IKeyValueAdapter {
1718
if (JsonKeyValueAdapter.#ExcludedJsonContentTypes.includes(setting.contentType)) {
1819
return false;
1920
}
20-
return isJsonContentType(setting.contentType);
21+
const contentType = parseContentType(setting.contentType);
22+
return isJsonContentType(contentType);
2123
}
2224

2325
async processKeyValue(setting: ConfigurationSetting): Promise<[string, unknown]> {
@@ -34,24 +36,3 @@ export class JsonKeyValueAdapter implements IKeyValueAdapter {
3436
return [setting.key, parsedValue];
3537
}
3638
}
37-
38-
// Determine whether a content type string is a valid JSON content type.
39-
// https://docs.microsoft.com/en-us/azure/azure-app-configuration/howto-leverage-json-content-type
40-
function isJsonContentType(contentTypeValue: string): boolean {
41-
if (!contentTypeValue) {
42-
return false;
43-
}
44-
45-
const contentTypeNormalized: string = contentTypeValue.trim().toLowerCase();
46-
const mimeType: string = contentTypeNormalized.split(";", 1)[0].trim();
47-
const typeParts: string[] = mimeType.split("/");
48-
if (typeParts.length !== 2) {
49-
return false;
50-
}
51-
52-
if (typeParts[0] !== "application") {
53-
return false;
54-
}
55-
56-
return typeParts[1].split("+").includes("json");
57-
}

src/common/contentType.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { secretReferenceContentType, featureFlagContentType } from "@azure/app-configuration";
5+
6+
export type ContentType = {
7+
mediaType: string;
8+
parameters: Record<string, string>;
9+
}
10+
11+
export function parseContentType(contentTypeValue: string | undefined): ContentType | undefined {
12+
if (!contentTypeValue) {
13+
return undefined;
14+
}
15+
const [mediaType, ...args] = contentTypeValue.split(";").map((s) => s.trim().toLowerCase());
16+
const parameters: Record<string, string> = {};
17+
18+
for (const param of args) {
19+
const [key, value] = param.split("=").map((s) => s.trim().toLowerCase());
20+
if (key && value) {
21+
parameters[key] = value;
22+
}
23+
}
24+
25+
return { mediaType, parameters };
26+
}
27+
28+
// Determine whether a content type string is a valid JSON content type.
29+
// https://docs.microsoft.com/en-us/azure/azure-app-configuration/howto-leverage-json-content-type
30+
export function isJsonContentType(contentType: ContentType | undefined): boolean {
31+
const mediaType = contentType?.mediaType;
32+
if (!mediaType) {
33+
return false;
34+
}
35+
36+
const typeParts: string[] = mediaType.split("/");
37+
if (typeParts.length !== 2) {
38+
return false;
39+
}
40+
41+
if (typeParts[0] !== "application") {
42+
return false;
43+
}
44+
45+
return typeParts[1].split("+").includes("json");
46+
}
47+
48+
export function isFeatureFlagContentType(contentType: ContentType | undefined): boolean {
49+
const mediaType = contentType?.mediaType;
50+
if (!mediaType) {
51+
return false;
52+
}
53+
return mediaType === featureFlagContentType;
54+
}
55+
56+
export function isSecretReferenceContentType(contentType: ContentType | undefined): boolean {
57+
const mediaType = contentType?.mediaType;
58+
if (!mediaType) {
59+
return false;
60+
}
61+
return mediaType === secretReferenceContentType;
62+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
export class AIConfigurationTracingOptions {
5+
usesAIConfiguration: boolean = false;
6+
usesAIChatCompletionConfiguration: boolean = false;
7+
8+
reset(): void {
9+
this.usesAIConfiguration = false;
10+
this.usesAIChatCompletionConfiguration = false;
11+
}
12+
13+
usesAnyTracingFeature() {
14+
return this.usesAIConfiguration || this.usesAIChatCompletionConfiguration;
15+
}
16+
}

src/requestTracing/FeatureFlagTracingOptions.ts

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class FeatureFlagTracingOptions {
1818
usesSeed: boolean = false;
1919
maxVariants: number = 0;
2020

21-
resetFeatureFlagTracing(): void {
21+
reset(): void {
2222
this.usesCustomFilter = false;
2323
this.usesTimeWindowFilter = false;
2424
this.usesTargetingFilter = false;
@@ -52,44 +52,27 @@ export class FeatureFlagTracingOptions {
5252
}
5353

5454
createFeatureFiltersString(): string {
55-
if (!this.usesAnyFeatureFilter()) {
56-
return "";
57-
}
58-
59-
let result: string = "";
55+
const tags: string[] = [];
6056
if (this.usesCustomFilter) {
61-
result += CUSTOM_FILTER_KEY;
57+
tags.push(CUSTOM_FILTER_KEY);
6258
}
6359
if (this.usesTimeWindowFilter) {
64-
if (result !== "") {
65-
result += DELIMITER;
66-
}
67-
result += TIME_WINDOW_FILTER_KEY;
60+
tags.push(TIME_WINDOW_FILTER_KEY);
6861
}
6962
if (this.usesTargetingFilter) {
70-
if (result !== "") {
71-
result += DELIMITER;
72-
}
73-
result += TARGETING_FILTER_KEY;
63+
tags.push(TARGETING_FILTER_KEY);
7464
}
75-
return result;
65+
return tags.join(DELIMITER);
7666
}
7767

7868
createFeaturesString(): string {
79-
if (!this.usesAnyTracingFeature()) {
80-
return "";
81-
}
82-
83-
let result: string = "";
69+
const tags: string[] = [];
8470
if (this.usesSeed) {
85-
result += FF_SEED_USED_TAG;
71+
tags.push(FF_SEED_USED_TAG);
8672
}
8773
if (this.usesTelemetry) {
88-
if (result !== "") {
89-
result += DELIMITER;
90-
}
91-
result += FF_TELEMETRY_USED_TAG;
74+
tags.push(FF_TELEMETRY_USED_TAG);
9275
}
93-
return result;
76+
return tags.join(DELIMITER);
9477
}
9578
}

src/requestTracing/constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,11 @@ export const FF_MAX_VARIANTS_KEY = "MaxVariants";
7070
export const FF_SEED_USED_TAG = "Seed";
7171
export const FF_FEATURES_KEY = "FFFeatures";
7272

73+
// AI Configuration tracing
74+
export const AI_CONFIGURATION_TAG = "AI";
75+
export const AI_CHAT_COMPLETION_CONFIGURATION_TAG = "AICC";
76+
77+
export const AI_MIME_PROFILE = "https://azconfig.io/mime-profiles/ai";
78+
export const AI_CHAT_COMPLETION_MIME_PROFILE = "https://azconfig.io/mime-profiles/ai/chat-completion";
79+
7380
export const DELIMITER = "+";

0 commit comments

Comments
 (0)