@komori-live/bonsai
v0.0.8
Published
Komori Bonsai SDK (Node.js / TypeScript)
Downloads
23
Readme
Komori Bonsai TypeScript SDK
A simple yet powerful logger for sending structured logs to Komori Live (the official cloud platform for Bonsai) or to your own self-hosted Bonsai instance. This SDK is designed for both server-side (Node.js) and client-side (Browsers, React, Vue, Angular) applications, with a strong focus on type safety, security, and ease of use.
✨ Features
- Type-Safe Structured Logging: Send rich, strongly-typed log messages with generic payloads (
IBonsaiLogger<TPayload>) for compile-time safety. - Efficient Log Batching: Automatically queue logs and send them in batches to reduce network overhead and improve performance.
- Flexible Authentication: Supports both long-lived API keys and short-lived JWTs with automatic auth scheme detection.
- Resilient Delivery: Automatically retries failed requests using exponential backoff with jitter.
- Secure by Design: Provides a clear pattern for protecting API keys in client-side applications via a
tokenProvider. - Asynchronous: All logging operations are non-blocking
Promise-based calls. - Isomorphic: Works seamlessly in both Node.js and browser environments.
- Interface-Based: Exported
IBonsaiLogger<TPayload>interface allows for easy mocking in unit tests.
📦 Installation
npm install @komori-live/bonsai
# or
yarn add @komori-live/bonsai🚀 Basic Usage
For most server-side use cases, you can create a logger without specifying a payload type. It will default to BonsaiDefaultPayload, which accepts any valid JSON object.
import { createBonsaiLogger, BonsaiLogLevel } from '@komori-live/bonsai';
// Create a logger with the default payload type
const logger = createBonsaiLogger({
projectId: process.env.BONSAI_PROJECT_ID!,
apiKey: process.env.BONSAI_API_KEY!,
source: 'my-simple-service',
});
// Use the new semantic methods for cleaner logging
await logger.info('User signed in', {
userId: 'usr_123',
from: 'google',
});
// Old way (still supported):
// await logger.log('User signed in', BonsaiLogLevel.Info, {
// userId: 'usr_123',
// from: 'google',
// });💪 Advanced Usage (Custom Payload Types)
For stricter type-safety, you can define an interface or type for your log payload and provide it as a generic argument to the factory function.
import {
createBonsaiLogger,
IBonsaiLogger,
BonsaiLogLevel,
} from '@komori-live/bonsai';
// 1. Define your structured payload
interface PurchasePayload {
userId: string;
productId: string;
amount: number;
}
// 2. Create a strongly-typed logger instance
const logger = createBonsaiLogger<PurchasePayload>({
projectId: process.env.BONSAI_PROJECT_ID!,
apiKey: process.env.BONSAI_API_KEY!,
source: 'my-backend-service',
});
// 3. Log with a type-safe payload
const payload: PurchasePayload = {
userId: 'usr_1234',
productId: 'prod_5678',
amount: 99.99,
};
await logger.info('User completed purchase', payload);
// This would cause a compile-time error:
// await logger.error('User failed purchase', { userId: 'usr_1234' });⚡ High-Performance Logging with Batching
For applications that generate a high volume of logs, such as a busy web server or a chatty client application, sending each log as a separate HTTP request can be inefficient. The SDK provides an optional log batching feature to address this.
When enabled, the logger adds logs to an in-memory queue. A background timer then flushes this queue periodically, sending multiple logs in a single request to a dedicated batching endpoint (/api/logs/batch). This significantly reduces network overhead and can improve the performance of both your application and the logging service.
Enabling Batching
To enable batching, set enableBatching to true in your BonsaiLoggerOptions. You can also customize the batchSize and flushIntervalMs.
const logger = createBonsaiLogger({
projectId: process.env.BONSAI_PROJECT_ID!,
apiKey: process.env.BONSAI_API_KEY!,
source: 'my-high-volume-app',
// Enable and configure batching
enableBatching: true,
batchSize: 200, // Default: 100
flushIntervalMs: 10000, // Default: 5000 (10 seconds)
});Manual Flushing and Cleanup
The background timer automatically flushes the queue. However, in some cases, you might want to manually trigger a flush to ensure all buffered logs are sent immediately. This is especially important in serverless functions or before a script exits.
The IBonsaiLogger interface provides a flush() method for this purpose. For long-lived applications (like a Node.js server), it's also important to clean up the timer when the logger is no longer needed. The destroy() method handles this.
// Manually flush the queue
await logger.flush();
// In a long-running application, clean up when you're done
process.on('beforeExit', async () => {
await logger.destroy();
});🛡️ Secure Client-Side Logging (Recommended)
Never expose your API key in a client-side application. The correct pattern is to use a secure backend to "vend" short-lived JWTs to the client.
The Bonsai SDK for TypeScript makes this easy with the tokenProvider option. You provide a function that knows how to fetch a token, and the SDK manages the entire token lifecycle for you, including initial fetching, renewal on expiry, and retrying failed logs.
The tokenProvider Pattern
- Your Backend API: Create an endpoint that uses its long-lived API key to ask Bonsai for a short-lived JWT.
- Your Frontend App: Configure the
BonsaiLoggerwith atokenProviderfunction that calls your backend endpoint to get the token. The SDK handles the rest.
Example: Angular LogService with tokenProvider
This example shows how to set up a type-safe logging service in a modern Angular application.
Define Payloads and Create a
LogService:// src/app/core/logging/log-payloads.ts export interface PageViewPayload { route: string; durationMs: number; } export interface ErrorPayload { error: string; component: string; } // A union type for all possible log payloads in the app export type AppLogPayload = PageViewPayload | ErrorPayload; // src/app/core/services/log.service.ts import { HttpClient } from '@angular/common/http'; import { inject, Injectable, OnDestroy } from '@angular/core'; import { createBonsaiLogger, IBonsaiLogger, BonsaiLogLevel, BonsaiLoggingTokenResponse, } from '@komori-live/bonsai'; import { firstValueFrom } from 'rxjs'; import { environment } from '../../../environments/environment'; import { AppLogPayload } from '../logging/log-payloads'; @Injectable({ providedIn: 'root' }) export class LogService implements OnDestroy { // The logger is strongly-typed with our AppLogPayload union type private readonly logger: IBonsaiLogger<AppLogPayload>; private readonly http = inject(HttpClient); constructor() { this.logger = createBonsaiLogger<AppLogPayload>({ projectId: environment.bonsai.projectId, tokenProvider: this.getToken.bind(this), source: 'my-angular-app', // Enable batching for performance in a SPA enableBatching: true, }); } ngOnDestroy(): void { // Clean up the logger when the service is destroyed. this.logger.destroy(); } private async getToken(): Promise<string> { const { token } = await firstValueFrom( this.http.get<BonsaiLoggingTokenResponse>('/api/logging/token'), ); return token; } // Public logging methods can now directly use the new semantic methods. public info<TPayload extends AppLogPayload>( message: string, payload: TPayload, ): void { this.logger.info(message, payload); } public warn<TPayload extends AppLogPayload>( message: string, payload: TPayload, ): void { this.logger.warn(message, payload); } public error<TPayload extends AppLogPayload>( message: string, payload: TPayload, ): void { this.logger.error(message, payload); } }Create a Backend Endpoint to Vend Tokens:
You'll need an API endpoint that your
getTokenfunction can call. Here's an example using a C#/.NET backend, which integrates seamlessly with theKomoriLive.Bonsai.NET SDK:// In your .NET API controller (e.g., LoggingController.cs) using KomoriLive.Bonsai; using Microsoft.AspNetCore.Mvc; [ApiController] [Route("api/logging")] public class LoggingController : ControllerBase { private readonly IBonsaiTokenService _tokenService; // Use the IBonsaiTokenService, which can be registered // in Program.cs with `services.AddBonsaiTokenService()`. public LoggingController(IBonsaiTokenService tokenService) { _tokenService = tokenService; } [HttpGet("token")] public async Task<IActionResult> GetToken() { var token = await _tokenService.GetTokenAsync(); if (token is null) { return Unauthorized("Could not retrieve Bonsai token."); } return Ok(new { Token = token }); } }
🔌 Integrating with Other Loggers
You can easily integrate Bonsai with your existing logging libraries like winston or pino by creating a custom "transport" or "stream".
Example: Winston Transport
import {
createBonsaiLogger,
IBonsaiLogger,
BonsaiLogLevel,
BonsaiLoggerOptions,
} from '@komori-live/bonsai';
import Transport from 'winston-transport';
import winston from 'winston';
// 1. Create the Bonsai Transport
class BonsaiTransport extends Transport {
// The underlying logger can accept any object-based payload from Winston
private bonsaiLogger: IBonsaiLogger;
constructor(
opts: Transport.TransportStreamOptions & {
bonsaiConfig: BonsaiLoggerOptions;
},
) {
super(opts);
this.bonsaiLogger = createBonsaiLogger(opts.bonsaiConfig);
}
// Map Winston log levels to Bonsai log levels
private mapLevel = (level: string): BonsaiLogLevel =>
({
error: BonsaiLogLevel.Error,
warn: BonsaiLogLevel.Warning,
info: BonsaiLogLevel.Info,
debug: BonsaiLogLevel.Debug,
})[level] ?? BonsaiLogLevel.Info;
log(info: any, callback: () => void) {
setImmediate(() => this.emit('logged', info));
const { level, message, ...meta } = info;
const bonsaiLevel = this.mapLevel(level);
// Use a switch to call the correct semantic method
switch (bonsaiLevel) {
case BonsaiLogLevel.Debug:
this.bonsaiLogger.debug(message, meta);
break;
case BonsaiLogLevel.Info:
this.bonsaiLogger.info(message, meta);
break;
case BonsaiLogLevel.Warning:
this.bonsaiLogger.warn(message, meta);
break;
case BonsaiLogLevel.Error:
this.bonsaiLogger.error(message, meta);
break;
case BonsaiLogLevel.Critical:
this.bonsaiLogger.critical(message, meta);
break;
}
callback();
}
// Make sure to destroy the underlying logger to clean up timers.
close() {
this.bonsaiLogger.destroy().then(() => this.emit('finish'));
}
}
// 2. Configure Winston to use the transport
const winstonLogger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.Console(),
new BonsaiTransport({
bonsaiConfig: {
projectId: process.env.BONSAI_PROJECT_ID!,
apiKey: process.env.BONSAI_API_KEY!,
source: 'my-winston-app',
enableBatching: true, // Batching is great for transports
},
}),
],
});🔧 Configuration (BonsaiLoggerOptions)
| Name | Type | Required | Default | Description |
| ----------------- | ------------------------ | -------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| projectId | string | Yes | - | Your Bonsai project ID. |
| apiKey | string | (*) | - | Your project's long-lived API key (for server-side use). |
| tokenProvider | () => Promise<string> | (*) | - | An async function that returns a JWT. The SDK will manage the token lifecycle for you. See Secure Client-Side Logging. |
| source | string | No | bonsai-js | A string identifying the source of the logs (e.g., my-app-name). |
| endpoint | string | No | https://komori.live | The URL of the Bonsai ingestion endpoint. |
| retryCount | number | No | 2 | The number of times to retry a failed log request. |
| retryDelayMs | number | No | 5000 | The base delay in milliseconds between retries. Uses exponential backoff with jitter. |
| timeoutMs | number | No | 10000 | The timeout in milliseconds for a log request. |
| onError | (err: unknown) => void | No | undefined | A callback function to handle errors that occur during logging. |
| enableBatching | boolean | No | false | Enables or disables log batching. |
| batchSize | number | No | 100 | The maximum number of logs to include in a single batch. Only used when enableBatching is true. |
| flushIntervalMs | number | No | 5000 | The maximum time in milliseconds to wait before sending a batch. Only used when enableBatching is true. |
(*): You must provide either an apiKey (for servers) or a tokenProvider (for clients), but not both.
API
createBonsaiLogger<TPayload>(options)
Factory function to create a new Bonsai logger.
It accepts an optional generic type argument, TPayload, to define the structure of your log payloads for strong type-safety.
- Default: If omitted,
TPayloaddefaults toBonsaiDefaultPayload, which allows any valid JSON object. - Custom Type: Provide your own interface for strict type-checking:
createBonsaiLogger<MyPayload>(...). - Any Object: To allow any object structure (less strict than the default), use
createBonsaiLogger<Record<string, unknown>>(...). - No Payload: To disallow payloads entirely, use
createBonsaiLogger<Record<string, never>>(...).
IBonsaiLogger<TPayload>.log(...)
The log method has two overloads:
log(message: string, level: BonsaiLogLevel, payload?: TPayload): Promise<void>Sends a log with a simple message string and a strongly-typed payload.
log(message: BonsaiLogMessage<TPayload>): Promise<void>Sends a pre-constructed
BonsaiLogMessage<TPayload>object.
IBonsaiLogger<TPayload> Semantic Methods
The logger also includes several convenience methods that map directly to BonsaiLogLevel values. These are the recommended way to send logs.
debug(message, [payload])info(message, [payload])warn(message, [payload])error(message, [payload])critical(message, [payload])
Each of these methods also supports an object-based overload for specifying a source override:
// Simple usage
logger.info('User logged in', { id: '123' });
// Advanced usage with source override
logger.info({
message: 'User logged in',
source: 'auth-module',
payload: { id: '123' },
});IBonsaiLogger<TPayload>.flush(): Promise<void>
Manually triggers a flush of the log queue. This is useful in scenarios where you want to ensure all buffered logs are sent immediately, such as before application shutdown. This method is only active when enableBatching is true.
IBonsaiLogger<TPayload>.destroy(): Promise<void>
Cleans up any resources used by the logger, such as timers for batching. It also performs a final flush to ensure no logs are lost. Call this method when the logger is no longer needed, for example, during application shutdown.
getLoggingToken(): Promise<string | undefined>
This method is only available when using a static apiKey on the server.
Exchanges your long-lived API key for a short-lived JWT. This is the primary mechanism for securely providing a logging token to a client-side application from your backend.
It will return the JWT string on success or undefined if an error occurs. Any errors will be passed to the configured onError handler.
Note: This method cannot be used when the logger is configured with a
tokenProvider.
