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
70 changes: 56 additions & 14 deletions src/AzureAppConfigurationImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ 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 { 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";
import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOptions.js";
import { AIConfigurationTracingOptions } from "./requestTracing/AIConfigurationTracingOptions.js";
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";
import { ConfigurationClientManager } from "./ConfigurationClientManager.js";

Expand Down Expand Up @@ -58,6 +60,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
#isFailoverRequest: boolean = false;
#featureFlagTracing: FeatureFlagTracingOptions | undefined;
#fmVersion: string | undefined;
#aiConfigurationTracing: AIConfigurationTracingOptions | undefined;

// Refresh
#refreshInProgress: boolean = false;
Expand Down Expand Up @@ -97,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();
}

Expand Down Expand Up @@ -178,7 +182,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
};
}

Expand Down Expand Up @@ -416,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]);
}

Expand Down Expand Up @@ -467,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))
Expand Down Expand Up @@ -633,12 +648,35 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
throw new Error("All clients failed to get configuration settings.");
}

async #processKeyValues(setting: ConfigurationSetting<string>): Promise<[string, unknown]> {
async #processKeyValue(setting: ConfigurationSetting<string>): Promise<[string, unknown]> {
this.#setAIConfigurationTracing(setting);

const [key, value] = await this.#processAdapters(setting);
const trimmedKey = this.#keyWithPrefixesTrimmed(key);
return [trimmedKey, value];
}

#setAIConfigurationTracing(setting: ConfigurationSetting<string>): 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) &&
!isFeatureFlagContentType(contentType) &&
!isSecretReferenceContentType(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;
}
}
}
}

async #processAdapters(setting: ConfigurationSetting<string>): Promise<[string, unknown]> {
for (const adapter of this.#adapters) {
if (adapter.canProcess(setting)) {
Expand Down Expand Up @@ -675,6 +713,20 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
};
}

this.#setFeatureFlagTracing(featureFlag);

return featureFlag;
}

#createFeatureFlagReference(setting: ConfigurationSetting<string>): 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) {
if (featureFlag[CONDITIONS_KEY_NAME] &&
featureFlag[CONDITIONS_KEY_NAME][CLIENT_FILTERS_KEY_NAME] &&
Expand All @@ -693,16 +745,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
this.#featureFlagTracing.usesSeed = true;
}
}

return featureFlag;
}

#createFeatureFlagReference(setting: ConfigurationSetting<string>): string {
let featureFlagReference = `${this.#clientManager.endpoint.origin}/kv/${setting.key}`;
if (setting.label && setting.label.trim().length !== 0) {
featureFlagReference += `?label=${setting.label}`;
}
return featureFlagReference;
}
}

Expand Down
25 changes: 3 additions & 22 deletions src/JsonKeyValueAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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]> {
Expand All @@ -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");
}
62 changes: 62 additions & 0 deletions src/common/contentType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { secretReferenceContentType, featureFlagContentType } from "@azure/app-configuration";

export type ContentType = {
mediaType: string;
parameters: Record<string, string>;
}

export function parseContentType(contentTypeValue: string | undefined): ContentType | undefined {
if (!contentTypeValue) {
return undefined;
}
const [mediaType, ...args] = contentTypeValue.split(";").map((s) => s.trim().toLowerCase());
const parameters: Record<string, string> = {};

for (const param of args) {
const [key, value] = param.split("=").map((s) => s.trim().toLowerCase());
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;
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");
}

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;
}
16 changes: 16 additions & 0 deletions src/requestTracing/AIConfigurationTracingOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 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;
}

usesAnyTracingFeature() {
return this.usesAIConfiguration || this.usesAIChatCompletionConfiguration;
}
}
37 changes: 10 additions & 27 deletions src/requestTracing/FeatureFlagTracingOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
7 changes: 7 additions & 0 deletions src/requestTracing/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "+";
Loading