fix: harden retained log streaming

This commit is contained in:
ajnart committed 2026-08-20 12:24:35 +02:00
1 parent ce61c87cd8
commit 90bf147901
11 files changed
+162 -86

No files matched your search

+27 -6
View File
@@ -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 });
+4 -20
View File
@@ -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.