Skip to main content

LumberjackHttpDriver

This log driver is used to send an HTTP request to a configured log store server.

Take this implementation as an advanced example of a log driver implementation. Some of the decisions made are not compatible with every existing log store.

Implementation

The following are complementary interfaces for the log driver implementation.

interface LumberjackHttpLog {
readonly formattedLog: string;
readonly level: LumberjackLogLevel;
readonly origin: string;
}

The LumberjackHttpLog is the object sent to the log store.

The rest of the LumberjackHttpDriver is defined as follows

@Injectable()
export class LumberjackHttpDriver<TPayload extends LumberjackLogPayload | void = void>
implements LumberjackLogDriver<TPayload>, OnDestroy
{
static readonly driverIdentifier = 'LumberjackHttpDriver';

readonly #http = inject(HttpClient);
readonly #ngZone = inject(NgZone);
readonly #subscriptions = new Subscription();

readonly config = inject(lumberjackHttpDriverConfigToken);

ngOnDestroy(): void {
this.#subscriptions.unsubscribe();
}

logCritical({ formattedLog, log }: LumberjackLogDriverLog<TPayload>): void {
this.#sendLog(formattedLog, log);
}

logDebug({ formattedLog, log }: LumberjackLogDriverLog<TPayload>): void {
this.#sendLog(formattedLog, log);
}

logError({ formattedLog, log }: LumberjackLogDriverLog<TPayload>): void {
this.#sendLog(formattedLog, log);
}

logInfo({ formattedLog, log }: LumberjackLogDriverLog<TPayload>): void {
this.#sendLog(formattedLog, log);
}

logTrace({ formattedLog, log }: LumberjackLogDriverLog<TPayload>): void {
this.#sendLog(formattedLog, log);
}

logWarning({ formattedLog, log }: LumberjackLogDriverLog<TPayload>): void {
this.#sendLog(formattedLog, log);
}

private #sendLog(formattedLog: string, log: LumberjackLog<TPayload>): void {
const { origin, retryOptions, storeUrl } = this.config;
const httpLog: LumberjackHttpLog<TPayload> = { formattedLog, origin, log };

this.ngZone.runOutsideAngular(() => {
this.#subscriptions.add(
this.#http
.post<void>(storeUrl, httpLog)
.pipe(retryWithDelay(retryOptions.maxRetries, retryOptions.delayMs))
// HTTP requests complete after the response is received, so there's no need to unsubscribe.
.subscribe(() => {
// No-op
})
);
});
}
}

The HTTP log driver receives an HttpClient, an NgZone for optimizations purpose, and a custom configuration object that extends the LumberjackLogDriverConfig.

export interface LumberjackHttpDriverConfig extends LumberjackLogDriverConfig {
/**
* The identifier of the app which emitted the log.
* This is used to organize logs on the log store.
*/
readonly origin: string;
/**
* The desired retry behavior options on failed requests.
*/
readonly retryOptions: LumberjackHttpDriverRetryOptions;
/**
* The url of the log store endpoint.
*
* The endpoint matching this url MUST support the POST method.
*/
readonly storeUrl: string;
}

export interface LumberjackHttpDriverRetryOptions {
readonly delayMs: number;
readonly maxRetries: number;
}

The sendLog method has been optimized to run outside Angular's NgZone, avoiding unnecessary change detection cycles.

provideLumberjackHttpDriver

The provideLumberjackHttpDriver is similar to the provideLumberjackConsoleDriver.

Novelty appears with the withHttpOptions and withHttpConfig functions that allow to allow to configure the driver.

Additionally, we can configure the underlaying HttpClient by passing any features it receives like interceptors.

export type LumberjackHttpDriverConfigurationKind = 'options' | 'config';
export type LumberjackHttpDriverConfiguration<Kind extends LumberjackHttpDriverConfigurationKind> = {
kind: Kind;
providers: EnvironmentProviders;
};

function makeLumberjackHttpConfiguration<Kind extends LumberjackHttpDriverConfigurationKind>(
kind: Kind,
providers: Provider[]
): LumberjackHttpDriverConfiguration<Kind> {
return {
kind,
providers: makeEnvironmentProviders(providers),
};
}

export function withHttpConfig(config: LumberjackHttpDriverConfig): LumberjackHttpDriverConfiguration<'config'> {
return makeLumberjackHttpConfiguration('config', [
{
provide: lumberjackHttpDriverConfigToken,
deps: [lumberjackLogDriverConfigToken],
useFactory: (logDriverConfig: LumberjackLogDriverConfig): LumberjackHttpDriverInternalConfig => ({
...logDriverConfig,
identifier: LumberjackHttpDriver.driverIdentifier,
...config,
}),
},
]);
}

export function withHttpOptions(options: LumberjackHttpDriverOptions): LumberjackHttpDriverConfiguration<'options'> {
return makeLumberjackHttpConfiguration('options', [
{
provide: lumberjackHttpDriverConfigToken,
deps: [lumberjackLogDriverConfigToken],
useFactory: (logDriverConfig: LumberjackLogDriverConfig): LumberjackHttpDriverInternalConfig => ({
...logDriverConfig,
identifier: LumberjackHttpDriver.driverIdentifier,
...options,
}),
},
]);
}

export type HttpClientFeatures = Parameters<typeof provideHttpClient>;

export function provideLumberjackHttpDriver<Kind extends LumberjackHttpDriverConfigurationKind>(
configuration: LumberjackHttpDriverConfiguration<Kind>,
...features: HttpClientFeatures
): EnvironmentProviders[] {
return [
provideHttpClient(...features),
makeEnvironmentProviders([lumberjackHttpDriverProvider]),
configuration.providers,
];
}

The following is an example of how both API can be used

bootstrapApplication(AppComponent, {
providers: [
provideLumberjack(),
provideLumberjackConsoleDriver(),
provideLumberjackHttpDriver(
withHttpOptions({
origin: 'ForestApp',
retryOptions: { maxRetries: 1, delayMs: 250 },
storeUrl: '/api/logs',
}),
withInterceptors([
(req, next) => {
const easy = inject(easyToken);
console.log('are interceptors working?', easy);
return next(req);
},
])
),