Skip to content

Commit 4e32c63

Browse files
CopilotAlCalzone
andauthored
Preserve existing log transports when modifying log config (#1559)
Co-authored-by: AlCalzone <17641229+AlCalzone@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Dominic Griesel <dominic.griesel@nabucasa.com>
1 parent d72fafc commit 4e32c63

5 files changed

Lines changed: 162 additions & 13 deletions

File tree

src/lib/driver/message_handler.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { IncomingMessageDriver } from "./incoming_message.js";
1212
import { DriverResultTypes } from "./outgoing_message.js";
1313
import { dumpDriver, dumpLogConfig } from "../state.js";
1414
import { MessageHandler } from "../message_handler.js";
15+
import { preserveLogTransports } from "../logging.js";
1516

1617
export class DriverMessageHandler implements MessageHandler {
1718
constructor(
@@ -47,7 +48,9 @@ export class DriverMessageHandler implements MessageHandler {
4748
return { config };
4849
}
4950
case DriverCommand.updateLogConfig: {
50-
this.driver.updateLogConfig(message.config);
51+
this.driver.updateLogConfig(
52+
preserveLogTransports(this.driver, message.config),
53+
);
5154
// If the logging event forwarder is enabled, we need to restart
5255
// it so that it picks up the new config.
5356
this.clientsController.restartLoggingEventForwarderIfNeeded();

src/lib/logging.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { MESSAGE as messageSymbol } from "triple-beam";
33
import { ConfigLogContext } from "@zwave-js/config";
44
import { NodeLogContext } from "@zwave-js/core";
55
import { createDefaultTransportFormat } from "@zwave-js/core/bindings/log/node";
6-
import type { ZWaveLogInfo } from "@zwave-js/core";
6+
import type { LogConfig, ZWaveLogInfo } from "@zwave-js/core";
77
import { SerialLogContext } from "@zwave-js/serial";
88
import { ClientsController, Logger } from "./server.js";
99
import { ControllerLogContext, DriverLogContext, Driver } from "zwave-js";
@@ -35,23 +35,24 @@ export class LoggingEventForwarder {
3535
}
3636

3737
start(filter?: Partial<LogContexts>) {
38-
const { transports = [], level } = this.driver.getLogConfig();
38+
const { level } = this.driver.getLogConfig();
3939
// Set the log level before attaching the transport
4040
this.logger.info("Starting logging event forwarder at " + level + " level");
4141
this.serverTransport = new WebSocketLogTransport(
4242
level as string,
4343
this.clients,
4444
filter,
4545
);
46+
const transports = [...(this.driver.getLogConfig().transports ?? [])];
4647
transports.push(this.serverTransport);
4748
this.driver.updateLogConfig({ transports });
4849
}
4950

5051
stop() {
5152
this.logger.info("Stopping logging event forwarder");
52-
const transports = this.driver
53-
.getLogConfig()
54-
.transports.filter((transport) => transport !== this.serverTransport);
53+
const transports = (this.driver.getLogConfig().transports ?? []).filter(
54+
(transport) => transport !== this.serverTransport,
55+
);
5556
this.driver.updateLogConfig({ transports });
5657
delete this.serverTransport;
5758
}
@@ -65,6 +66,16 @@ export class LoggingEventForwarder {
6566
}
6667
}
6768

69+
export const preserveLogTransports = (
70+
driver: Driver,
71+
config: Partial<LogConfig>,
72+
): Partial<LogConfig> => {
73+
if (!("transports" in config)) return config;
74+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
75+
const { transports: _, ...rest } = config;
76+
return { ...rest, transports: [...(driver.getLogConfig().transports ?? [])] };
77+
};
78+
6879
class WebSocketLogTransport extends Transport {
6980
public constructor(
7081
level: string,

src/lib/server.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ import {
3838
import { Instance } from "./instance.js";
3939
import { ServerCommand } from "./command.js";
4040
import { DriverMessageHandler } from "./driver/message_handler.js";
41-
import { LogContexts, LoggingEventForwarder } from "./logging.js";
41+
import {
42+
LogContexts,
43+
LoggingEventForwarder,
44+
preserveLogTransports,
45+
} from "./logging.js";
4246
import { BroadcastNodeMessageHandler } from "./broadcast_node/message_handler.js";
4347
import { MulticastGroupMessageHandler } from "./multicast_group/message_handler.js";
4448
import { EndpointMessageHandler } from "./endpoint/message_handler.js";
@@ -165,7 +169,9 @@ export class Client {
165169
}
166170

167171
if (msg.command === ServerCommand.updateLogConfig) {
168-
this.driver.updateLogConfig(msg.config);
172+
this.driver.updateLogConfig(
173+
preserveLogTransports(this.driver, msg.config),
174+
);
169175
this.sendResultSuccess(msg.messageId, {});
170176
return;
171177
}

src/mock/index.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,29 @@ class MockDriver extends EventEmitter {
1818

1919
public statisticsEnabled = true;
2020

21+
private logConfig: Partial<LogConfig> = {
22+
enabled: true,
23+
level: "debug",
24+
transports: [],
25+
};
26+
2127
async start() {
2228
this.emit("driver ready");
2329
}
2430

2531
public getLogConfig(): Partial<LogConfig> {
2632
return {
27-
enabled: true,
28-
level: "debug",
29-
transports: [],
33+
...this.logConfig,
34+
transports: [...(this.logConfig.transports ?? [])],
3035
};
3136
}
3237

33-
public updateLogConfig(_config: Partial<LogConfig>) {}
38+
public updateLogConfig(config: Partial<LogConfig>) {
39+
this.logConfig = {
40+
...this.logConfig,
41+
...config,
42+
};
43+
}
3444

3545
public updateUserAgent(
3646
_additionalUserAgentComponents?: Record<string, string> | null,

src/test/integration.ts

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as assert from "assert";
22
import dns from "node:dns";
33
import ws from "ws";
4+
import type { LogConfig } from "@zwave-js/core";
5+
import Transport from "winston-transport";
46
import { libVersion } from "zwave-js";
57
import { ZwavejsServer } from "../lib/server.js";
68
import { createMockDriver } from "../mock/index.js";
@@ -11,6 +13,7 @@ const require = createRequire(import.meta.url);
1113
dns.setDefaultResultOrder("ipv4first");
1214

1315
const PORT = 45001;
16+
type LogTransport = LogConfig["transports"][number];
1417

1518
const createNextMessage = (socket: ws) => {
1619
let waitingListener: ((msg: unknown) => void) | undefined;
@@ -37,8 +40,50 @@ const createNextMessage = (socket: ws) => {
3740
};
3841
};
3942

43+
const waitForResult = async (
44+
nextMessage: ReturnType<typeof createNextMessage>,
45+
messageId: string,
46+
) => {
47+
for (;;) {
48+
const message = await Promise.race([
49+
nextMessage(),
50+
new Promise((_, reject) =>
51+
setTimeout(
52+
() =>
53+
reject(
54+
new Error(
55+
`Timed out while waiting for result message ${messageId}`,
56+
),
57+
),
58+
5000,
59+
),
60+
),
61+
]);
62+
if (
63+
typeof message === "object" &&
64+
message !== undefined &&
65+
message !== null &&
66+
"type" in message &&
67+
message.type === "result" &&
68+
"messageId" in message &&
69+
message.messageId === messageId
70+
) {
71+
return message;
72+
}
73+
}
74+
};
75+
76+
class MockTransport extends Transport {
77+
public log(_info: unknown, next: () => void): void {
78+
next();
79+
}
80+
}
81+
4082
const runTest = async () => {
41-
const server = new ZwavejsServer(createMockDriver(), { port: PORT });
83+
const driver = createMockDriver();
84+
const customTransport = new MockTransport() as LogTransport;
85+
driver.updateLogConfig({ transports: [customTransport] });
86+
const server = new ZwavejsServer(driver, { port: PORT });
4287
await server.start(true);
4388
let socket: ws | undefined = undefined;
4489

@@ -97,6 +142,80 @@ const runTest = async () => {
97142
},
98143
});
99144

145+
socket.send(
146+
JSON.stringify({
147+
messageId: "start-listening-logs",
148+
command: "driver.start_listening_logs",
149+
}),
150+
);
151+
152+
assert.deepEqual(await nextMessage(), {
153+
type: "result",
154+
success: true,
155+
messageId: "start-listening-logs",
156+
result: {},
157+
});
158+
159+
assert.equal(
160+
driver.getLogConfig().transports?.includes(customTransport),
161+
true,
162+
);
163+
assert.equal(driver.getLogConfig().transports?.length, 2);
164+
165+
socket.send(
166+
JSON.stringify({
167+
messageId: "update-log-config",
168+
command: "driver.update_log_config",
169+
config: {
170+
enabled: true,
171+
level: "info",
172+
transports: [],
173+
},
174+
}),
175+
);
176+
177+
assert.deepEqual(await nextMessage(), {
178+
type: "event",
179+
event: {
180+
source: "driver",
181+
event: "log config updated",
182+
config: {
183+
enabled: true,
184+
level: "info",
185+
},
186+
},
187+
});
188+
189+
assert.deepEqual(await waitForResult(nextMessage, "update-log-config"), {
190+
type: "result",
191+
success: true,
192+
messageId: "update-log-config",
193+
result: {},
194+
});
195+
196+
assert.equal(driver.getLogConfig().level, "info");
197+
assert.equal(
198+
driver.getLogConfig().transports?.includes(customTransport),
199+
true,
200+
);
201+
assert.equal(driver.getLogConfig().transports?.length, 2);
202+
203+
socket.send(
204+
JSON.stringify({
205+
messageId: "stop-listening-logs",
206+
command: "driver.stop_listening_logs",
207+
}),
208+
);
209+
210+
assert.deepEqual(await nextMessage(), {
211+
type: "result",
212+
success: true,
213+
messageId: "stop-listening-logs",
214+
result: {},
215+
});
216+
217+
assert.deepEqual(driver.getLogConfig().transports, [customTransport]);
218+
100219
console.log("Integration tests passed :)");
101220
} finally {
102221
if (socket) {

0 commit comments

Comments
 (0)