UNPKG

hypertune

Version:

[Hypertune](https://www.hypertune.com/) is the most flexible platform for feature flags, A/B testing, analytics and app configuration. Built with full end-to-end type-safety, Git-style version control and local, synchronous, in-memory flag evaluation. Opt

269 lines (245 loc) 7.4 kB
import { Expression, sdkVersion, LocalLogger, CreateLogsInput, LogLevel, LogType, TracedFetch, } from "../shared"; import { LogsHandler, Logs, RemoteLoggingMode } from "../shared/types"; import RemoteLogger from "../shared/helpers/RemoteLogger"; import LRUCache from "../shared/helpers/LRUCache"; import getNodeCacheKey from "./getNodeCacheKey"; import newTracedFetch from "../shared/helpers/newTracedFetch"; const fetchMaxKeepAliveRequestSizeBytes = 64_000; /** * Logger provides a high level API for Node and generic SDK logs. It emits * these logs using the `localLogger` and it also forwards them to the * `remoteLogger` based on the provided `remoteLoggingMode`. */ export default class Logger { public readonly id: string; private readonly remoteLoggingMode: RemoteLoggingMode; private readonly remoteLogCache: LRUCache<boolean> = new LRUCache(10_000); private readonly remoteLogger: RemoteLogger | null; private readonly localLogger: LocalLogger; private readonly logsHandler: LogsHandler; constructor({ id, traceId, token, remoteLoggingMode, remoteFlushIntervalMs, remoteLoggingEndpointUrl, localLogger, logsHandler, }: { id: string; traceId: string; token: string; remoteLoggingMode: RemoteLoggingMode; remoteFlushIntervalMs: number | null; remoteLoggingEndpointUrl: string; logsHandler: LogsHandler; localLogger: LocalLogger; }) { this.id = id; this.remoteLoggingMode = remoteLoggingMode; this.logsHandler = logsHandler; this.localLogger = localLogger; this.remoteLogger = remoteLoggingMode === "off" ? null : new RemoteLogger({ traceId, token, createLogs: getCreateLogsFunction( newTracedFetch({ timeoutMs: 20_000, localLogger: this.localLogger, }), remoteLoggingEndpointUrl ), localLogger: this.localLogger, flushIntervalMs: remoteFlushIntervalMs, }); if (remoteLoggingMode === "off") { this.localLogger(LogLevel.Info, "Remote logging is disabled.", { traceId, }); } } public nodeLog({ commitId, initDataHash, nodeTypeName, nodePath, nodeExpression, reductionLogs, }: { commitId: string | null; initDataHash: string | null; nodeTypeName: string; nodePath: string; nodeExpression: Expression | null; reductionLogs: Logs; }): void { const baseLogMetadata = { sdkVersion, nodeTypeName, nodePath, nodeExpression, reductionLogs, }; this.logsHandler({ messageList: reductionLogs.messageList ?? [], eventList: reductionLogs.eventList ?? [], exposureList: reductionLogs.exposureList ?? [], evaluationList: reductionLogs.evaluationList ?? [], }); reductionLogs.messageList?.forEach(({ level, message, metadata }) => { const logMessage = `${nodeTypeName}Node at ${nodePath}: ${message}`; const logMetadata = { ...baseLogMetadata, ...metadata }; if ( (level === LogLevel.Warn || level === LogLevel.Error) && this.shouldRemoteNodeLog(initDataHash, nodePath, message) ) { this.remoteLogger?.log( LogType.SdkNode, level, commitId, logMessage, logMetadata ); } }); if ( reductionLogs && (reductionLogs.evaluations || reductionLogs.eventList || reductionLogs.exposureList) ) { if (!commitId) { const errorMessage = `${nodeTypeName}Node at ${nodePath}: Missing commitId so cannot remote log evaluations, events and exposures.`; this.localLogger(LogLevel.Error, errorMessage, baseLogMetadata); if (this.shouldRemoteNodeLog(initDataHash, nodePath, errorMessage)) { this.remoteLogger?.log( LogType.SdkNode, LogLevel.Error, commitId, errorMessage, baseLogMetadata ); } return; } if ( reductionLogs.evaluations && this.shouldRemoteNodeLog(initDataHash, nodePath, "evaluations") ) { this.remoteLogger?.evaluations(commitId, reductionLogs.evaluations); } if (reductionLogs.eventList) { // We always log events to the backend this.remoteLogger?.events(commitId, reductionLogs.eventList); } if ( reductionLogs.exposureList && this.shouldRemoteNodeLog(initDataHash, nodePath, "exposures") ) { this.remoteLogger?.exposures(commitId, reductionLogs.exposureList); } } } private shouldRemoteNodeLog( initDataHash: string | null, nodePath: string, cacheKeySuffix: string ): boolean { switch (this.remoteLoggingMode) { case "session": { const cacheKey = getNodeCacheKey( initDataHash ?? "", nodePath, cacheKeySuffix ); if (this.remoteLogCache.get(cacheKey)) { this.localLogger( LogLevel.Debug, `Remote log cache hit.`, /* metadata */ { initDataHash, nodePath, cacheKeySuffix } ); return false; } this.remoteLogCache.set(cacheKey, true); return true; } case "normal": { return true; } case "off": { return false; } default: { const neverLoggingMode: never = this.remoteLoggingMode; throw new Error(`Unexpected logging mode: ${neverLoggingMode}`); } } } public log( level: LogLevel, commitId: string | null, message: string, metadata: object ): void { this.localLogger(level, message, metadata); if ( this.remoteLoggingMode !== "off" && (level === LogLevel.Warn || level === LogLevel.Error) ) { this.remoteLogger?.log(LogType.SdkMessage, level, commitId, message, { ...metadata, sdkVersion, }); } } public flush(traceId: string): Promise<void> { return this.remoteLogger ? this.remoteLogger.flush(traceId) : Promise.resolve(); } public close(traceId: string): Promise<void> { return this.remoteLogger ? this.remoteLogger.close(traceId) : Promise.resolve(); } } function getCreateLogsFunction(tracedFetch: TracedFetch, logsUrl: string) { return async (traceId: string, input: CreateLogsInput) => { const bodyJson = JSON.stringify(input); const bodyBlob = new Blob([bodyJson]); const response = await tracedFetch(traceId, logsUrl, { method: "POST", headers: { "Content-Type": "application/json", "Cache-Control": "no-store", }, body: bodyJson, // Only use keepalive if the request is smaller than the maximum // allowed size with keepalive enabled. keepalive: bodyBlob.size < fetchMaxKeepAliveRequestSizeBytes, }); if (!response.ok) { throw new Error( `Failed to create logs status: "${response.status}" response: "${await response.text()}"` ); } const data: { success: boolean } = await response.json(); if (!data.success) { throw new Error( `Failed to create logs status: "${response.status}" response: "${JSON.stringify(data)}"` ); } }; }