npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@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

  1. Your Backend API: Create an endpoint that uses its long-lived API key to ask Bonsai for a short-lived JWT.
  2. Your Frontend App: Configure the BonsaiLogger with a tokenProvider function 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.

  1. 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);
      }
    }
  2. Create a Backend Endpoint to Vend Tokens:

    You'll need an API endpoint that your getToken function can call. Here's an example using a C#/.NET backend, which integrates seamlessly with the KomoriLive.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, TPayload defaults to BonsaiDefaultPayload, 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:

  1. log(message: string, level: BonsaiLogLevel, payload?: TPayload): Promise<void>

    Sends a log with a simple message string and a strongly-typed payload.

  2. 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.