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
text/typescript
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)}"`
);
}
};
}