import { createClient } from "@clickhouse/client"; import { env } from "../../env"; import { NodeClickHouseClientConfigOptions } from "@clickhouse/client/dist/config"; import { getCurrentSpan } from "../instrumentation"; import { propagation, context } from "@opentelemetry/api"; export type ClickhouseClientType = ReturnType; export type PreferredClickhouseService = "ReadWrite" | "ReadOnly"; /** * ClickHouseClientManager provides a singleton pattern for managing ClickHouse clients. * It creates and reuses clients based on their configuration to avoid creating * a new connection for each query. */ export class ClickHouseClientManager { private static instance: ClickHouseClientManager; private clientMap: Map = new Map(); /** * Private constructor to enforce singleton pattern */ private constructor() {} /** * Get the singleton instance of the ClickHouseClientManager */ public static getInstance(): ClickHouseClientManager { if (!ClickHouseClientManager.instance) { ClickHouseClientManager.instance = new ClickHouseClientManager(); } return ClickHouseClientManager.instance; } /** * Generate a consistent hash key for client configurations * @param opts Client parameters * @returns String hash key */ private generateClientSettings( opts: NodeClickHouseClientConfigOptions, preferredClickhouseService: PreferredClickhouseService = "ReadWrite", ): NodeClickHouseClientConfigOptions { const keyParams = { url: this.getClickhouseUrl(preferredClickhouseService), username: env.CLICKHOUSE_USER, password: env.CLICKHOUSE_PASSWORD, database: env.CLICKHOUSE_DB, http_headers: opts?.http_headers ?? {}, settings: opts?.clickhouse_settings, ...(opts.request_timeout ? { request_timeout: opts.request_timeout } : {}), // Include any other relevant config options }; return keyParams; } private generateClientSettingsKey( settings: NodeClickHouseClientConfigOptions, ): string { return JSON.stringify(settings); } private getClickhouseUrl = ( preferredClickhouseService: PreferredClickhouseService, ) => { return preferredClickhouseService === "ReadWrite" ? env.CLICKHOUSE_URL : env.CLICKHOUSE_READ_ONLY_URL || env.CLICKHOUSE_URL; }; /** * Get or create a client based on the provided parameters * @param opts Client configuration parameters * @returns ClickHouse client instance */ public getClient( opts: NodeClickHouseClientConfigOptions, preferredClickhouseService: PreferredClickhouseService = "ReadWrite", ): ClickhouseClientType { const settings = this.generateClientSettings( opts, preferredClickhouseService, ); const key = this.generateClientSettingsKey(settings); if (!this.clientMap.has(key)) { const activeSpan = getCurrentSpan(); if (activeSpan) { propagation.inject(context.active(), settings.http_headers); } const cloudOptions: Record = {}; if ( ["STAGING", "EU", "US", "HIPAA"].includes( env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION ?? "", ) ) { cloudOptions.input_format_json_throw_on_bad_escape_sequence = 0; } const client = createClient({ ...opts, ...settings, keep_alive: { idle_socket_ttl: env.CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL, }, max_open_connections: env.CLICKHOUSE_MAX_OPEN_CONNECTIONS, clickhouse_settings: { ...cloudOptions, ...opts.clickhouse_settings, async_insert: 1, wait_for_async_insert: 1, // if disabled, we won't get errors from clickhouse ...(opts.request_timeout && opts.request_timeout > 30000 ? { send_progress_in_http_headers: 1, http_headers_progress_interval_ms: "25000", // UInt64, should be passed as a string } : {}), }, }); this.clientMap.set(key, client); } return this.clientMap.get(key)!; } /** * Close all client connections - useful for application shutdown */ public closeAllConnections(): Promise { const closePromises = Array.from(this.clientMap.values()).map((client) => client.close(), ); this.clientMap.clear(); return Promise.all(closePromises); } } export const clickhouseClient = ( opts?: NodeClickHouseClientConfigOptions, preferredClickhouseService: PreferredClickhouseService = "ReadWrite", ) => { return ClickHouseClientManager.getInstance().getClient( opts ?? {}, preferredClickhouseService, ); }; /** * Accepts a JavaScript date and returns the DateTime in format YYYY-MM-DD HH:MM:SS */ export const convertDateToClickhouseDateTime = (date: Date): string => { // 2024-11-06T20:37:00.123Z -> 2024-11-06 21:37:00.123 return date.toISOString().replace("T", " ").replace("Z", ""); };