Skip to content

Commit

Permalink
chore: improve logger, parsing and add helpers (#689)
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Beemer <[email protected]>
  • Loading branch information
beeme1mr authored Dec 13, 2023
1 parent e0dbfdb commit fa0a238
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 157 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ export class InProcessService implements Service {
private _flagdCore: FlagdCore;
private _dataFetcher: DataFetch;

constructor(config: Config, dataFetcher?: DataFetch, logger?: Logger) {
this._flagdCore = new FlagdCore();
constructor(
config: Config,
dataFetcher?: DataFetch,
private logger?: Logger,
) {
this._flagdCore = new FlagdCore(undefined, logger);
this._dataFetcher = dataFetcher ? dataFetcher : new GrpcFetch(config, undefined, logger);
}

Expand Down Expand Up @@ -63,6 +67,10 @@ export class InProcessService implements Service {
}

private fill(flags: string): void {
this._flagdCore.setConfigurations(flags);
try {
this._flagdCore.setConfigurations(flags);
} catch (err) {
this.logger?.error(err);
}
}
}
120 changes: 94 additions & 26 deletions libs/shared/flagd-core/src/lib/flagd-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,85 +5,152 @@ import {
FlagValue,
GeneralError,
JsonValue,
FlagValueType,
ResolutionDetails,
StandardResolutionReasons,
TypeMismatchError,
Logger,
SafeLogger,
DefaultLogger,
} from '@openfeature/core';
import { Targeting } from './targeting/targeting';
import { Logger } from '@openfeature/server-sdk';
import { FeatureFlag } from './feature-flag';

/**
* Expose flag configuration setter and flag resolving methods.
*/
export class FlagdCore {
export class FlagdCore implements Storage {
private _logger: Logger;
private _storage: Storage;
private _targeting = new Targeting();
private _targeting: Targeting;

/**
* Optionally construct with your own storage layer.
*/
constructor(storage?: Storage) {
constructor(storage?: Storage, logger?: Logger) {
this._storage = storage ? storage : new MemoryStorage();
this._logger = logger ? new SafeLogger(logger) : new DefaultLogger();
this._targeting = new Targeting(this._logger);
}

/**
* Add flag configurations to the storage.
* Sets the logger for the FlagdCore instance.
* @param logger - The logger to be set.
* @returns - The FlagdCore instance with the logger set.
*/
setLogger(logger: Logger) {
this._logger = new SafeLogger(logger);
return this;
}

setConfigurations(cfg: string): void {
this._storage.setConfigurations(cfg);
}

getFlag(key: string): FeatureFlag | undefined {
return this._storage.getFlag(key);
}

getFlags(): Map<string, FeatureFlag> {
return this._storage.getFlags();
}

/**
* Resolve the flag evaluation to a boolean value.
* @param flagKey - The key of the flag to be evaluated.
* @param defaultValue - The default value to be returned if the flag is not found.
* @param evalCtx - The evaluation context to be used for targeting.
* @param logger - The logger to be used to troubleshoot targeting errors. Overrides the default logger.
* @returns - The resolved value and the reason for the resolution.
*/
resolveBooleanEvaluation(
flagKey: string,
defaultValue: boolean,
evalCtx: EvaluationContext,
logger: Logger,
evalCtx?: EvaluationContext,
logger?: Logger,
): ResolutionDetails<boolean> {
return this.resolve(flagKey, defaultValue, evalCtx, logger, 'boolean');
return this.resolve('boolean', flagKey, defaultValue, evalCtx, logger);
}

/**
* Resolve the flag evaluation to a string value.
* @param flagKey - The key of the flag to be evaluated.
* @param defaultValue - The default value to be returned if the flag is not found.
* @param evalCtx - The evaluation context to be used for targeting.
* @param logger - The logger to be used to troubleshoot targeting errors. Overrides the default logger.
* @returns - The resolved value and the reason for the resolution.
*/
resolveStringEvaluation(
flagKey: string,
defaultValue: string,
evalCtx: EvaluationContext,
logger: Logger,
evalCtx?: EvaluationContext,
logger?: Logger,
): ResolutionDetails<string> {
return this.resolve(flagKey, defaultValue, evalCtx, logger, 'string');
return this.resolve('string', flagKey, defaultValue, evalCtx, logger);
}

/**
* Resolve the flag evaluation to a numeric value.
* @param flagKey - The key of the flag to evaluate.
* @param defaultValue - The default value to return if the flag is not found or the evaluation fails.
* @param evalCtx - The evaluation context to be used for targeting.
* @param logger - The logger to be used to troubleshoot targeting errors. Overrides the default logger.
* @returns - The resolved value and the reason for the resolution.
*/
resolveNumberEvaluation(
flagKey: string,
defaultValue: number,
evalCtx: EvaluationContext,
logger: Logger,
evalCtx?: EvaluationContext,
logger?: Logger,
): ResolutionDetails<number> {
return this.resolve(flagKey, defaultValue, evalCtx, logger, 'number');
return this.resolve('number', flagKey, defaultValue, evalCtx, logger);
}

/**
* Resolve the flag evaluation to an object value.
* @template T - The type of the return value.
* @param flagKey - The key of the flag to resolve.
* @param defaultValue - The default value to use if the flag is not found.
* @param evalCtx - The evaluation context to be used for targeting.
* @param logger - The logger to be used to troubleshoot targeting errors. Overrides the default logger.
* @returns - The resolved value and the reason for the resolution.
*/
resolveObjectEvaluation<T extends JsonValue>(
flagKey: string,
defaultValue: T,
evalCtx: EvaluationContext,
logger: Logger,
evalCtx?: EvaluationContext,
logger?: Logger,
): ResolutionDetails<T> {
return this.resolve(flagKey, defaultValue, evalCtx, logger, 'object');
return this.resolve('object', flagKey, defaultValue, evalCtx, logger);
}

private resolve<T extends FlagValue>(
/**
* Resolves the value of a flag based on the specified type type.
* @template T - The type of the flag value.
* @param {FlagValueType} type - The type of the flag value.
* @param {string} flagKey - The key of the flag.
* @param {T} defaultValue - The default value of the flag.
* @param {EvaluationContext} evalCtx - The evaluation context for targeting rules.
* @param {Logger} [logger] - The optional logger for logging errors.
* @returns {ResolutionDetails<T>} - The resolved value and the reason for the resolution.
* @throws {FlagNotFoundError} - If the flag with the given key is not found.
* @throws {TypeMismatchError} - If the evaluated type of the flag does not match the expected type.
* @throws {GeneralError} - If the variant specified in the flag is not found.
*/
resolve<T extends FlagValue>(
type: FlagValueType,
flagKey: string,
defaultValue: T,
evalCtx: EvaluationContext,
logger: Logger,
type: string,
evalCtx: EvaluationContext = {},
logger?: Logger,
): ResolutionDetails<T> {
// flag exist check
logger ??= this._logger;
const flag = this._storage.getFlag(flagKey);
// flag exist check
if (!flag) {
throw new FlagNotFoundError(`flag: ${flagKey} not found`);
throw new FlagNotFoundError(`flag: '${flagKey}' not found`);
}

// flag status check
if (flag.state === 'DISABLED') {
logger.debug(`Flag ${flagKey} is disabled, returning default value ${defaultValue}`);
return {
value: defaultValue,
reason: StandardResolutionReasons.DISABLED,
Expand All @@ -94,6 +161,7 @@ export class FlagdCore {
let reason;

if (!flag.targeting) {
logger.debug(`Flag ${flagKey} has no targeting rules`);
variant = flag.defaultVariant;
reason = StandardResolutionReasons.STATIC;
} else {
Expand Down
4 changes: 2 additions & 2 deletions libs/shared/flagd-core/src/lib/parser.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Ajv from 'ajv';
import { FeatureFlag, Flag } from './feature-flag';
import mydata from '../../flagd-schemas/json/flagd-definitions.json';
import flagDefinitionSchema from '../../flagd-schemas/json/flagd-definitions.json';

const ajv = new Ajv();
const matcher = ajv.compile(mydata);
const matcher = ajv.compile(flagDefinitionSchema);

const evaluatorKey = '$evaluators';
const bracketReplacer = new RegExp('^[^{]*\\{|}[^}]*$', 'g');
Expand Down
26 changes: 21 additions & 5 deletions libs/shared/flagd-core/src/lib/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,25 @@ import { parse } from './parser';
* The simple contract of the storage layer.
*/
export interface Storage {
/**
* Sets the configurations from the given string.
* @param cfg The configuration string to be parsed and stored.
* @throws {Error} If the configuration string is invalid.
*/
setConfigurations(cfg: string): void;

/**
* Gets the feature flag configuration with the given key.
* @param key The key of the flag to be retrieved.
* @returns The flag with the given key or undefined if not found.
*/
getFlag(key: string): FeatureFlag | undefined;

/**
* Gets all the feature flag configurations.
* @returns The map of all the flags.
*/
getFlags(): Map<string, FeatureFlag>;
}

/**
Expand All @@ -24,11 +40,11 @@ export class MemoryStorage implements Storage {
return this._flags.get(key);
}

getFlags(): Map<string, FeatureFlag> {
return this._flags;
}

setConfigurations(cfg: string): void {
try {
this._flags = parse(cfg);
} catch (e) {
console.error(e);
}
this._flags = parse(cfg);
}
}
95 changes: 49 additions & 46 deletions libs/shared/flagd-core/src/lib/targeting/fractional.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,70 @@
import { flagdPropertyKey, flagKeyPropertyKey, targetingPropertyKey } from './common';
import MurmurHash3 from 'imurmurhash';
import type { EvaluationContext, EvaluationContextValue, Logger } from '@openfeature/core';

export const fractionalRule = 'fractional';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function fractional(data: unknown, context: Record<any, any>): string | null {
if (!Array.isArray(data)) {
return null;
}

const args = Array.from(data);
if (args.length < 2) {
console.error('Invalid targeting rule. Require at least two buckets.');
return null;
}

const flagdProperties = context[flagdPropertyKey];
if (!flagdProperties) {
return null;
}
export function fractionalFactory(logger: Logger) {
return function fractional(data: unknown, context: EvaluationContext): string | null {
if (!Array.isArray(data)) {
return null;
}

let bucketBy: string;
let buckets: unknown[];
const args = Array.from(data);
if (args.length < 2) {
logger.debug(`Invalid ${fractionalRule} configuration: Expected at least 2 buckets, got ${args.length}`);
return null;
}

if (typeof args[0] == 'string') {
bucketBy = args[0];
buckets = args.slice(1, args.length);
} else {
bucketBy = context[targetingPropertyKey];
if (!bucketBy) {
console.error('Missing targetingKey property');
const flagdProperties = context[flagdPropertyKey] as { [key: string]: EvaluationContextValue };
if (!flagdProperties) {
logger.debug('Missing flagd properties, cannot perform fractional targeting');
return null;
}

buckets = args;
}
let bucketBy: string | undefined;
let buckets: unknown[];

let bucketingList;
if (typeof args[0] == 'string') {
bucketBy = args[0];
buckets = args.slice(1, args.length);
} else {
bucketBy = context[targetingPropertyKey];
if (!bucketBy) {
logger.debug('Missing targetingKey property, cannot perform fractional targeting');
return null;
}

try {
bucketingList = toBucketingList(buckets);
} catch (e) {
console.error('Error parsing targeting rule', e);
return null;
}
buckets = args;
}

const hashKey = flagdProperties[flagKeyPropertyKey] + bucketBy;
// hash in signed 32 format. Bitwise operation here works in signed 32 hence the conversion
const hash = new MurmurHash3(hashKey).result() | 0;
const bucket = (Math.abs(hash) / 2147483648) * 100;
let bucketingList;

let sum = 0;
for (let i = 0; i < bucketingList.length; i++) {
const bucketEntry = bucketingList[i];
try {
bucketingList = toBucketingList(buckets);
} catch (err) {
logger.debug(`Invalid ${fractionalRule} configuration: `, (err as Error).message);
return null;
}

const hashKey = flagdProperties[flagKeyPropertyKey] + bucketBy;
// hash in signed 32 format. Bitwise operation here works in signed 32 hence the conversion
const hash = new MurmurHash3(hashKey).result() | 0;
const bucket = (Math.abs(hash) / 2147483648) * 100;

let sum = 0;
for (let i = 0; i < bucketingList.length; i++) {
const bucketEntry = bucketingList[i];

sum += bucketEntry.fraction;
sum += bucketEntry.fraction;

if (sum >= bucket) {
return bucketEntry.variant;
if (sum >= bucket) {
return bucketEntry.variant;
}
}
}

return null;
return null;
};
}

function toBucketingList(from: unknown[]): { variant: string; fraction: number }[] {
Expand Down
Loading

0 comments on commit fa0a238

Please sign in to comment.