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
40 changes: 30 additions & 10 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ERROR_REASON, GENERAL_ERROR } from './constants';
import { OpenFeature } from './open-feature';
import { SafeLogger } from './logger';
import {
Client,
ClientMetadata,
Expand All @@ -10,6 +11,7 @@ import {
FlagValueType,
Hook,
HookContext,
Logger,
Provider,
ResolutionDetails,
} from './types';
Expand All @@ -23,11 +25,13 @@ export class OpenFeatureClient implements Client {
readonly metadata: ClientMetadata;
private _context: EvaluationContext;
private _hooks: Hook[] = [];
private _clientLogger?: Logger;

constructor(
// we always want the client to use the current provider,
// so pass a function to always access the currently registered one.
private readonly providerAccessor: () => Provider,
private readonly globalLogger: () => Logger,
options: OpenFeatureClientOptions,
context: EvaluationContext = {}
) {
Expand All @@ -38,11 +42,19 @@ export class OpenFeatureClient implements Client {
this._context = context;
}

set context (context: EvaluationContext) {
set logger(logger: Logger) {
this._clientLogger = new SafeLogger(logger);
}

get logger(): Logger {
return this._clientLogger || this.globalLogger();
}

set context(context: EvaluationContext) {
this._context = context;
}

get context (): EvaluationContext {
get context(): EvaluationContext {
return this._context;
}

Expand Down Expand Up @@ -153,7 +165,12 @@ export class OpenFeatureClient implements Client {

private async evaluate<T extends FlagValue>(
flagKey: string,
resolver: (flagKey: string, defaultValue: T, context: EvaluationContext) => Promise<ResolutionDetails<T>>,
resolver: (
flagKey: string,
defaultValue: T,
context: EvaluationContext,
logger: Logger
) => Promise<ResolutionDetails<T>>,
defaultValue: T,
flagType: FlagValueType,
invocationContext: EvaluationContext = {},
Expand All @@ -180,13 +197,14 @@ export class OpenFeatureClient implements Client {
clientMetadata: this.metadata,
providerMetadata: OpenFeature.providerMetadata,
context: mergedContext,
logger: this.logger,
};

try {
const frozenContext = await this.beforeHooks(allHooks, hookContext, options);

// run the referenced resolver, binding the provider.
const resolution = await resolver.call(this.provider, flagKey, defaultValue, frozenContext);
const resolution = await resolver.call(this.provider, flagKey, defaultValue, frozenContext, this.logger);

const evaluationDetails = {
...resolution,
Expand Down Expand Up @@ -246,9 +264,10 @@ export class OpenFeatureClient implements Client {
try {
await hook?.error?.(hookContext, err, options.hookHints);
} catch (err) {
// TODO: replace with injected logger
console.error(`Unhandled error during 'error' hook: ${err}`);
console.error((err as Error).stack);
this.logger.error(`Unhandled error during 'error' hook: ${err}`);
if (err instanceof Error) {
this.logger.error(err.stack);
}
}
}
}
Expand All @@ -259,9 +278,10 @@ export class OpenFeatureClient implements Client {
try {
await hook?.finally?.(hookContext, options.hookHints);
} catch (err) {
// TODO: replace with injected logger
console.error(`Unhandled error during 'finally' hook: ${err}`);
console.error((err as Error).stack);
this.logger.error(`Unhandled error during 'finally' hook: ${err}`);
if (err instanceof Error) {
this.logger.error(err.stack);
}
}
}
}
Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './open-feature';
export * from './client';
export * from './types';
export * from './errors/index';
55 changes: 55 additions & 0 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { Logger } from './types';

const LOG_LEVELS: Array<keyof Logger> = ['error', 'warn', 'info', 'debug'];
export class DefaultLogger implements Logger {
error(...args: unknown[]): void {
console.error(...args);
}
warn(...args: unknown[]): void {
console.warn(...args);
}
info(): void {}
debug(): void {}
}

export class SafeLogger implements Logger {
private readonly logger: Logger;
private readonly fallbackLogger = new DefaultLogger();

constructor(logger: Logger) {
try {
for (const level of LOG_LEVELS) {
if (!logger[level] || typeof logger[level] !== 'function') {
throw new Error(`The provided logger is missing the ${level} method.`);
}
}
this.logger = logger;
} catch (err) {
console.error(err);
console.error('Falling back to the default logger.');
this.logger = this.fallbackLogger;
}
}

error(...args: unknown[]): void {
this.log('error', ...args);
}
warn(...args: unknown[]): void {
this.log('warn', ...args);
}
info(...args: unknown[]): void {
this.log('info', ...args);
}
debug(...args: unknown[]): void {
this.log('debug', ...args);
}

private log(level: keyof Logger, ...args: unknown[]) {
try {
this.logger[level](...args);
} catch (error) {
this.fallbackLogger[level](...args);
}
}
}
19 changes: 17 additions & 2 deletions src/open-feature.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { OpenFeatureClient } from './client';
import { DefaultLogger, SafeLogger } from './logger';
import { NOOP_PROVIDER } from './no-op-provider';
import { Client, EvaluationContext, EvaluationLifeCycle, FlagValue, Hook, Provider } from './types';
import { Client, EvaluationContext, EvaluationLifeCycle, FlagValue, Hook, Logger, Provider } from './types';

// use a symbol as a key for the global singleton
const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/js.api');
Expand All @@ -14,6 +15,7 @@ class OpenFeatureAPI implements EvaluationLifeCycle {
private _provider: Provider = NOOP_PROVIDER;
private _context: EvaluationContext = {};
private _hooks: Hook[] = [];
private _logger: Logger = new DefaultLogger();

static getInstance(): OpenFeatureAPI {
const globalApi = _global[GLOBAL_OPENFEATURE_API_KEY];
Expand All @@ -26,8 +28,21 @@ class OpenFeatureAPI implements EvaluationLifeCycle {
return instance;
}

set logger(logger: Logger) {
this._logger = new SafeLogger(logger);
}

get logger() {
return this._logger;
}

getClient(name?: string, version?: string, context?: EvaluationContext): Client {
return new OpenFeatureClient(() => this._provider, { name, version }, context);
return new OpenFeatureClient(
() => this._provider,
() => this._logger,
{ name, version },
context
);
}

get providerMetadata() {
Expand Down
21 changes: 17 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ export interface FlagEvaluationOptions {
hookHints?: HookHints;
}

export interface Logger {
error(...args: unknown[]): void;
warn(...args: unknown[]): void;
info(...args: unknown[]): void;
debug(...args: unknown[]): void;
}

export interface Features {
/**
* Get a boolean flag value.
Expand Down Expand Up @@ -118,7 +125,8 @@ export interface Provider extends Pick<Partial<EvaluationLifeCycle>, 'hooks'> {
resolveBooleanEvaluation(
flagKey: string,
defaultValue: boolean,
context: EvaluationContext
context: EvaluationContext,
logger: Logger
): Promise<ResolutionDetails<boolean>>;

/**
Expand All @@ -127,7 +135,8 @@ export interface Provider extends Pick<Partial<EvaluationLifeCycle>, 'hooks'> {
resolveStringEvaluation(
flagKey: string,
defaultValue: string,
context: EvaluationContext
context: EvaluationContext,
logger: Logger
): Promise<ResolutionDetails<string>>;

/**
Expand All @@ -136,7 +145,8 @@ export interface Provider extends Pick<Partial<EvaluationLifeCycle>, 'hooks'> {
resolveNumberEvaluation(
flagKey: string,
defaultValue: number,
context: EvaluationContext
context: EvaluationContext,
logger: Logger
): Promise<ResolutionDetails<number>>;

/**
Expand All @@ -145,7 +155,8 @@ export interface Provider extends Pick<Partial<EvaluationLifeCycle>, 'hooks'> {
resolveObjectEvaluation<U extends object>(
flagKey: string,
defaultValue: U,
context: EvaluationContext
context: EvaluationContext,
logger: Logger
): Promise<ResolutionDetails<U>>;
}

Expand Down Expand Up @@ -212,6 +223,7 @@ export type EvaluationDetails<T extends FlagValue> = {
export interface Client extends EvaluationLifeCycle, Features {
readonly metadata: ClientMetadata;
context: EvaluationContext;
logger: Logger;
}

export type HookHints = Readonly<Record<string, unknown>>;
Expand All @@ -234,6 +246,7 @@ export interface HookContext<T extends FlagValue = FlagValue> {
readonly context: Readonly<EvaluationContext>;
readonly clientMetadata: ClientMetadata;
readonly providerMetadata: ProviderMetadata;
readonly logger: Logger;
}

export interface BeforeHookContext extends HookContext {
Expand Down
Loading