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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ types/
*.tgz
tsconfig.tsbuildinfo
.envrc

# IDE and tool directories
.idea/
.claude/
.yarn/
2 changes: 1 addition & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
nodeLinker: node-modules
npmRegistryServer: 'https://registry.npmjs.org'
npmAuthToken: '${NPM_AUTH_TOKEN}'
npmAuthToken: '${NPM_AUTH_TOKEN-}'
196 changes: 190 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ setTimeout(ping, reforge.get('ping-delay'));
| `isEnabled` | `reforge.isEnabled("new-logo")` | returns a boolean (default `false`) if a feature is enabled based on the current context |
| `get` | `reforge.get('retry-count')` | returns the value of a flag or config evaluated in the current context |
| `getDuration` | `reforge.getDuration('http.timeout')` | returns a duration object `{seconds: number, ms: number}` |
| `getLogLevel` | `reforge.getLogLevel("my.app.logger")` | returns a `LogLevel` enum value for the specified logger name |
| `logger` | `reforge.logger.info("message")` | log messages with dynamic log level control (see below for all methods) |
| `loaded` | `if (reforge.loaded) { ... }` | a boolean indicating whether reforge content has loaded |
| `shouldLog` | `if (reforge.shouldLog(...)) {` | returns a boolean indicating whether the proposed log level is valid for the current context |
| `poll` | `reforge.poll({frequencyInMs})` | starts polling every `frequencyInMs` ms. |
Expand All @@ -91,10 +93,12 @@ setTimeout(ping, reforge.get('ping-delay'));
| `desiredLevel` | string | INFO | No |
| `defaultLevel` | string | ERROR | No |

If you've configured a level value for `loggerName` (or a parent in the dot-notation hierarchy like
"my.corp.widgets") then that value will be used for comparison against the `desiredLevel`. If no
configured level is found in the hierarchy for `loggerName` then the provided `defaultLevel` will be
compared against `desiredLevel`.
If you've configured a level value for the exact `loggerName` (as `log-level.{loggerName}`), that
value will be used for comparison against the `desiredLevel`. If no configured level is found for
the exact `loggerName`, then the provided `defaultLevel` will be compared against `desiredLevel`.

**Note:** `shouldLog` does NOT traverse the logger name hierarchy. It only checks for an exact match
of `log-level.{loggerName}`.

If `desiredLevel` is greater than or equal to the comparison severity, then `shouldLog` returns
true. If the `desiredLevel` is less than the comparison severity, then `shouldLog` will return
Expand All @@ -112,10 +116,190 @@ if (shouldLog({ loggerName, desiredLevel, defaultLevel })) {
}
```

If no log level value is configured in Reforge for "my.corp.widgets.modal" or higher in the
hierarchy, then the `console.info` will not happen. If the value is configured and is INFO or more
If no log level value is configured in Reforge for the exact key
`"log-level.my.corp.widgets.modal"`, then the `defaultLevel` ("ERROR") will be used and the
`console.info` will not happen. If the value is configured for that exact key and is INFO or more
verbose, the `console.info` will happen.

## `getLogLevel()`

`getLogLevel` provides a simpler way to get log levels for dynamic logging. It returns a `LogLevel`
enum value from a configured key.

### Configuration

You can optionally specify a custom logger key during initialization (default is
`"log-levels.default"`):

```javascript
await reforge.init({
sdkKey: "1234",
context: new Context({
/* ... */
}),
loggerKey: "my.custom.log.config", // optional, defaults to "log-levels.default"
});
```

### Usage

```javascript
import { reforge, LogLevel } from "@reforge-com/javascript";

const loggerName = "my.app.widgets.modal";
const level = reforge.getLogLevel(loggerName);

// level is a LogLevel enum value
if (level === LogLevel.DEBUG || level === LogLevel.TRACE) {
console.debug("Debug information...");
}
```

### How it works

When you call `getLogLevel(loggerName)`, the method:

1. Looks up the configured logger key (default: `"log-levels.default"`)
2. Returns the appropriate `LogLevel` enum value (TRACE, DEBUG, INFO, WARN, ERROR, or FATAL)
3. Returns `LogLevel.DEBUG` as the default if no configuration is found

**Note:** The `loggerName` parameter is currently only used for potential telemetry/logging
purposes. All loggers share the same configured log level from the logger key. For per-logger log
levels, use `shouldLog()` with individual `log-level.{loggerName}` configs.

## `logger` - Simple Logging Methods

The `reforge.logger` object provides convenient methods for logging at different levels. These
methods automatically check the configured log level and only output to the console when
appropriate.

### Available Methods

```javascript
import { reforge } from "@reforge-com/javascript";

// Configure the log level
await reforge.init({
sdkKey: "1234",
context: new Context({
/* ... */
}),
loggerKey: "log-levels.default", // optional
});

reforge.hydrate({ "log-levels.default": "INFO" });

// Use the logger methods
reforge.logger.trace("Trace message"); // Will not log (below INFO)
reforge.logger.debug("Debug message"); // Will not log (below INFO)
reforge.logger.info("Info message"); // Will log
reforge.logger.warn("Warning message"); // Will log
reforge.logger.error("Error message"); // Will log
reforge.logger.fatal("Fatal message"); // Will log
```

### How it Works

Each logger method:

1. Checks the configured log level from the logger key (default: `"log-levels.default"`)
2. Compares the message level against the configured level
3. Only outputs to the console if the level is enabled

**Console Method Mapping:**

- `trace()` and `debug()` → `console.debug()`
- `info()` → `console.info()`
- `warn()` → `console.warn()`
- `error()` and `fatal()` → `console.error()`

### Example

```javascript
import { reforge, Context } from "@reforge-com/javascript";

await reforge.init({
sdkKey: "your-key",
context: new Context({ user: { id: "123" } }),
});

// Set log level to WARN
reforge.hydrate({ "log-levels.default": "WARN" });

reforge.logger.debug("Debug details"); // Not logged
reforge.logger.info("Process started"); // Not logged
reforge.logger.warn("Low disk space"); // Logged to console
reforge.logger.error("Failed to save"); // Logged to console
```

### LogLevel enum values

- `LogLevel.TRACE` (1) - Most verbose
- `LogLevel.DEBUG` (2)
- `LogLevel.INFO` (3)
- `LogLevel.WARN` (5)
- `LogLevel.ERROR` (6)
- `LogLevel.FATAL` (9) - Least verbose

### Comparing LogLevel values

Since `LogLevel` is a string enum, you can't use `<=` directly. Use the provided helper functions:

```javascript
import { reforge, LogLevel, shouldLogAtLevel, getLogLevelSeverity } from "@reforge-com/javascript";

const configuredLevel = reforge.getLogLevel("my.app.logger");

// Option 1: Use shouldLogAtLevel helper (recommended)
if (shouldLogAtLevel(configuredLevel, LogLevel.DEBUG)) {
console.debug("Debug message");
}

// Option 2: Compare severity values
if (getLogLevelSeverity(configuredLevel) <= getLogLevelSeverity(LogLevel.INFO)) {
console.info("Info message");
}
```

### Example integration

```javascript
import { reforge, LogLevel, shouldLogAtLevel } from "@reforge-com/javascript";

class Logger {
constructor(name) {
this.name = name;
}

debug(message) {
const level = reforge.getLogLevel(this.name);
if (shouldLogAtLevel(level, LogLevel.DEBUG)) {
console.debug(`[${this.name}] ${message}`);
}
}

info(message) {
const level = reforge.getLogLevel(this.name);
if (shouldLogAtLevel(level, LogLevel.INFO)) {
console.info(`[${this.name}] ${message}`);
}
}

error(message) {
const level = reforge.getLogLevel(this.name);
if (shouldLogAtLevel(level, LogLevel.ERROR)) {
console.error(`[${this.name}] ${message}`);
}
}
}

// Usage
const logger = new Logger("my.app.components.modal");
logger.debug("Modal opened"); // Only logs if DEBUG level is enabled for this logger
logger.info("User action completed"); // Only logs if INFO level or more verbose is enabled
logger.error("Failed to save"); // Logs for ERROR level or more verbose
```

## `poll()`

After `reforge.init()`, you can start polling. Polling uses the context you defined in `init` by
Expand Down
13 changes: 12 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { reforge, Reforge, ReforgeInitParams, ReforgeBootstrap } from "./src/reforge";
import { Config } from "./src/config";
import Context from "./src/context";
import { LogLevel, getLogLevelSeverity, shouldLogAtLevel } from "./src/logger";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { version } = require("./package.json");

export { reforge, Reforge, ReforgeInitParams, Config, Context, version };
export {
reforge,
Reforge,
ReforgeInitParams,
Config,
Context,
LogLevel,
getLogLevelSeverity,
shouldLogAtLevel,
version,
};

export { ReforgeBootstrap };

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"packageManager": "yarn@4.9.2",
"name": "@reforge-com/javascript",
"version": "0.0.3",
"version": "0.0.4",
"description": "Feature Flags & Dynamic Configuration as a Service",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
Expand Down
38 changes: 24 additions & 14 deletions src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,28 +31,38 @@ export interface ShouldLogParams {
get: <K extends keyof TypedFrontEndConfigurationRaw>(key: K) => TypedFrontEndConfigurationRaw[K];
}

// LogLevel enum for public API
export enum LogLevel {
TRACE = "TRACE",
DEBUG = "DEBUG",
INFO = "INFO",
WARN = "WARN",
ERROR = "ERROR",
FATAL = "FATAL",
}

// Get the numeric severity value for a LogLevel (lower = more verbose)
export const getLogLevelSeverity = (level: LogLevel): number => WORD_LEVEL_LOOKUP[level];

// Check if a log at desiredLevel should be logged given the configured level
// Returns true if desiredLevel is at or above the configured level's severity
export const shouldLogAtLevel = (configuredLevel: LogLevel, desiredLevel: LogLevel): boolean =>
WORD_LEVEL_LOOKUP[configuredLevel] <= WORD_LEVEL_LOOKUP[desiredLevel];

export const shouldLog = ({
loggerName,
desiredLevel,
defaultLevel,
get,
}: ShouldLogParams): boolean => {
let loggerNameWithPrefix = `${PREFIX}.${loggerName}`;

while (loggerNameWithPrefix.length > 0) {
const resolvedLevel = get(loggerNameWithPrefix);

if (resolvedLevel) {
return (
WORD_LEVEL_LOOKUP[resolvedLevel.toString().toUpperCase() as LogLevelWord] <= desiredLevel
);
}
const loggerNameWithPrefix = `${PREFIX}.${loggerName}`;

if (loggerNameWithPrefix.indexOf(".") === -1) {
break;
}
const resolvedLevel = get(loggerNameWithPrefix);

loggerNameWithPrefix = loggerNameWithPrefix.slice(0, loggerNameWithPrefix.lastIndexOf("."));
if (resolvedLevel) {
return (
WORD_LEVEL_LOOKUP[resolvedLevel.toString().toUpperCase() as LogLevelWord] <= desiredLevel
);
}

return defaultLevel <= desiredLevel;
Expand Down
Loading