fix: harden retained log streaming
This commit is contained in:
1 parent
ce61c87cd8
commit
90bf147901
11 files changed
+162
-86
No files matched your search
@@ -1,9 +1,15 @@
|
||||
import { parse } from "superjson";
|
||||
|
||||
import type { LoggerMessage } from "@homarr/core/infrastructure/logs/constants";
|
||||
import { LOG_HISTORY_KEY, LOG_HISTORY_MAX_ENTRIES } from "@homarr/core/infrastructure/logs/constants";
|
||||
import {
|
||||
loggerMessageSchema,
|
||||
LOG_HISTORY_KEY,
|
||||
LOG_HISTORY_MAX_ENTRIES,
|
||||
LOG_PUBLISH_CHANNEL,
|
||||
} from "@homarr/core/infrastructure/logs/constants";
|
||||
|
||||
import { createListChannel, createSubPubChannel } from "./lib/channel";
|
||||
import { ChannelSubscriptionTracker } from "./lib/channel-subscription-tracker";
|
||||
import { createRedisConnection } from "./lib/connection";
|
||||
|
||||
export {
|
||||
@@ -21,14 +27,29 @@ export const pingUrlChannel = createListChannel<string>("ping-url");
|
||||
|
||||
export type { LoggerMessage } from "@homarr/core/infrastructure/logs/constants";
|
||||
|
||||
const liveLoggingChannel = createSubPubChannel<LoggerMessage>("logging", { persist: false });
|
||||
const loggingHistoryClient = createRedisConnection();
|
||||
|
||||
const parseLoggerMessage = (message: string): LoggerMessage | null => {
|
||||
try {
|
||||
const result = loggerMessageSchema.safeParse(parse<unknown>(message));
|
||||
return result.success ? result.data : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const loggingChannel = {
|
||||
subscribe: liveLoggingChannel.subscribe,
|
||||
subscribeAsync: liveLoggingChannel.subscribeAsync,
|
||||
subscribeAsync: (callback: (message: LoggerMessage) => void) =>
|
||||
ChannelSubscriptionTracker.subscribeAsync(LOG_PUBLISH_CHANNEL, (message) => {
|
||||
const parsedMessage = parseLoggerMessage(message);
|
||||
if (parsedMessage) callback(parsedMessage);
|
||||
}),
|
||||
getRecentAsync: async () => {
|
||||
const messages = await loggingHistoryClient.lrange(LOG_HISTORY_KEY, 0, LOG_HISTORY_MAX_ENTRIES - 1);
|
||||
return messages.map((message) => parse<LoggerMessage>(message)).toReversed();
|
||||
const messages =
|
||||
(await loggingHistoryClient?.lrange(LOG_HISTORY_KEY, 0, LOG_HISTORY_MAX_ENTRIES - 1).catch(() => [])) ?? [];
|
||||
return messages
|
||||
.map(parseLoggerMessage)
|
||||
.filter((message): message is LoggerMessage => message !== null)
|
||||
.toReversed();
|
||||
},
|
||||
};
|
||||
@@ -58,14 +58,18 @@ export class ChannelSubscriptionTracker {
|
||||
|
||||
const channelSubscriptions = this.subscriptions.get(channelName) ?? new Map<string, SubscriptionCallback>();
|
||||
const id = randomUUID();
|
||||
let ready = this.subscriptionReadiness.get(channelName) ?? Promise.resolve();
|
||||
let ready = this.subscriptionReadiness.get(channelName);
|
||||
|
||||
// If there are no subscriptions to the channel, subscribe to it
|
||||
if (channelSubscriptions.size === 0) {
|
||||
// Subscribe when this channel has no active or pending Redis subscription.
|
||||
if (!ready) {
|
||||
logger.debug("Subscribing to redis channel", { channel: channelName });
|
||||
ready = this.redis.subscribe(channelName).then(() => undefined);
|
||||
this.subscriptionReadiness.set(channelName, ready);
|
||||
void ready.catch(() => undefined);
|
||||
void ready.catch(() => {
|
||||
if (this.subscriptionReadiness.get(channelName) === ready) {
|
||||
this.subscriptionReadiness.delete(channelName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug("Adding redis channel callback", { channel: channelName, id });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import superjson, { parse } from "superjson";
|
||||
import superjson from "superjson";
|
||||
|
||||
import { createId } from "@homarr/common";
|
||||
|
||||
@@ -8,12 +8,6 @@ import { createRedisConnection } from "./connection";
|
||||
const publisher = createRedisConnection();
|
||||
const lastDataClient = createRedisConnection();
|
||||
|
||||
const deserializeCallback =
|
||||
<TData>(callback: (data: TData) => void) =>
|
||||
(message: string) => {
|
||||
callback(parse(message));
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new pub/sub channel.
|
||||
* @param name name of the channel
|
||||
@@ -35,19 +29,9 @@ export const createSubPubChannel = <TData>(name: string, { persist }: { persist:
|
||||
}
|
||||
});
|
||||
}
|
||||
return ChannelSubscriptionTracker.subscribe(channelName, deserializeCallback(callback));
|
||||
},
|
||||
/**
|
||||
* Subscribes to the channel and resolves once Redis confirms the subscription.
|
||||
*/
|
||||
subscribeAsync: async (callback: (data: TData) => void) => {
|
||||
if (persist) {
|
||||
const data = await lastDataClient.get(lastChannelName);
|
||||
if (data) {
|
||||
callback(superjson.parse(data));
|
||||
}
|
||||
}
|
||||
return await ChannelSubscriptionTracker.subscribeAsync(channelName, deserializeCallback(callback));
|
||||
return ChannelSubscriptionTracker.subscribe(channelName, (message) => {
|
||||
callback(superjson.parse(message));
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Publish data to the channel with last data saved.
|
||||
|
||||
Reference in new issue
Block a user