Skip to content

Commit ae1bdc0

Browse files
committed
Add enable_logger_service option for runtime log level control
1 parent b527ca3 commit ae1bdc0

7 files changed

Lines changed: 266 additions & 1 deletion

File tree

lib/logging_service.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
'use strict';
2+
3+
const Logging = require('./logging.js');
4+
5+
const LOGGING_SEVERITY_UNSET = 0;
6+
7+
/**
8+
* Implements the ROS 2 logging service interfaces for a node.
9+
*
10+
* The interfaces implemented are:
11+
* rcl_interfaces/srv/GetLoggerLevels
12+
* rcl_interfaces/srv/SetLoggerLevels
13+
*
14+
* @class
15+
*/
16+
class LoggingService {
17+
/**
18+
* Create a new instance.
19+
* @param {Node} node - The node these services support.
20+
*/
21+
constructor(node) {
22+
this._node = node;
23+
this._isRunning = false;
24+
}
25+
26+
/**
27+
* Get the node this service supports.
28+
* @return {Node} - The supported node.
29+
*/
30+
get node() {
31+
return this._node;
32+
}
33+
34+
/**
35+
* Check if logging services are configured and accepting requests.
36+
* @return {boolean} - True if services are active; false otherwise.
37+
*/
38+
isStarted() {
39+
return this._isRunning;
40+
}
41+
42+
/**
43+
* Configure logging services and begin processing client requests.
44+
* @return {undefined}
45+
*/
46+
start() {
47+
if (this._isRunning) return;
48+
49+
this._isRunning = true;
50+
const nodeName = this.node.name();
51+
52+
this.node.createService(
53+
'rcl_interfaces/srv/GetLoggerLevels',
54+
nodeName + '/get_logger_levels',
55+
(request, response) => this._handleGetLoggerLevels(request, response)
56+
);
57+
58+
this.node.createService(
59+
'rcl_interfaces/srv/SetLoggerLevels',
60+
nodeName + '/set_logger_levels',
61+
(request, response) => this._handleSetLoggerLevels(request, response)
62+
);
63+
}
64+
65+
_handleGetLoggerLevels(request, response) {
66+
const msg = response.template;
67+
68+
for (const name of request.names) {
69+
try {
70+
msg.levels.push({
71+
name,
72+
level: Logging.getLogger(name).loggerEffectiveLevel,
73+
});
74+
} catch {
75+
msg.levels.push({
76+
name,
77+
level: LOGGING_SEVERITY_UNSET,
78+
});
79+
}
80+
}
81+
82+
response.send(msg);
83+
}
84+
85+
_handleSetLoggerLevels(request, response) {
86+
const msg = response.template;
87+
88+
for (const loggerLevel of request.levels) {
89+
const result = {
90+
successful: false,
91+
reason: '',
92+
};
93+
94+
try {
95+
Logging.getLogger(loggerLevel.name).setLoggerLevel(loggerLevel.level);
96+
result.successful = true;
97+
} catch (error) {
98+
result.reason = error.message;
99+
}
100+
101+
msg.results.push(result);
102+
}
103+
104+
response.send(msg);
105+
}
106+
}
107+
108+
module.exports = LoggingService;

lib/node.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const DistroUtils = require('./distro.js');
2525
const GuardCondition = require('./guard_condition.js');
2626
const loader = require('./interface_loader.js');
2727
const Logging = require('./logging.js');
28+
const LoggingService = require('./logging_service.js');
2829
const NodeOptions = require('./node_options.js');
2930
const {
3031
ParameterType,
@@ -121,6 +122,8 @@ class Node extends rclnodejs.ShadowNode {
121122
defaults.startTypeDescriptionService,
122123
enableRosout: options.enableRosout ?? defaults.enableRosout,
123124
rosoutQos: options.rosoutQos ?? defaults.rosoutQos,
125+
enableLoggerService:
126+
options.enableLoggerService ?? defaults.enableLoggerService,
124127
};
125128
}
126129

@@ -159,6 +162,7 @@ class Node extends rclnodejs.ShadowNode {
159162
this._parameterDescriptors = new Map();
160163
this._parameters = new Map();
161164
this._parameterService = null;
165+
this._loggerService = null;
162166
this._typeDescriptionService = null;
163167
this._parameterEventPublisher = null;
164168
this._preSetParametersCallbacks = [];
@@ -219,6 +223,11 @@ class Node extends rclnodejs.ShadowNode {
219223
this._parameterService.start();
220224
}
221225

226+
if (options.enableLoggerService) {
227+
this._loggerService = new LoggingService(this);
228+
this._loggerService.start();
229+
}
230+
222231
if (
223232
DistroUtils.getDistroId() >= DistroUtils.getDistroId('jazzy') &&
224233
options.startTypeDescriptionService

lib/node_options.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,16 @@ class NodeOptions {
2929
* @param {boolean} [startTypeDescriptionService=true]
3030
* @param {boolean} [enableRosout=true]
3131
* @param {QoS} [rosoutQos=QoS.profileDefault]
32+
* @param {boolean} [enableLoggerService=false]
3233
*/
3334
constructor(
3435
startParameterServices = true,
3536
parameterOverrides = [],
3637
automaticallyDeclareParametersFromOverrides = false,
3738
startTypeDescriptionService = true,
3839
enableRosout = true,
39-
rosoutQos = null
40+
rosoutQos = null,
41+
enableLoggerService = false
4042
) {
4143
this._startParameterServices = startParameterServices;
4244
this._parameterOverrides = parameterOverrides;
@@ -45,6 +47,7 @@ class NodeOptions {
4547
this._startTypeDescriptionService = startTypeDescriptionService;
4648
this._enableRosout = enableRosout;
4749
this._rosoutQos = rosoutQos;
50+
this._enableLoggerService = enableLoggerService;
4851
}
4952

5053
/**
@@ -164,6 +167,23 @@ class NodeOptions {
164167
this._rosoutQos = rosoutQos;
165168
}
166169

170+
/**
171+
* Get the enableLoggerService option.
172+
* Default value = false;
173+
* @returns {boolean} - true if logger services are enabled.
174+
*/
175+
get enableLoggerService() {
176+
return this._enableLoggerService;
177+
}
178+
179+
/**
180+
* Set enableLoggerService.
181+
* @param {boolean} enableLoggerService
182+
*/
183+
set enableLoggerService(enableLoggerService) {
184+
this._enableLoggerService = enableLoggerService;
185+
}
186+
167187
/**
168188
* Return an instance configured with default options.
169189
* @returns {NodeOptions} - An instance with default values.

test/test-logging-service.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright (c) 2026, The Robot Web Tools Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
const assert = require('assert');
18+
const sinon = require('sinon');
19+
const Logging = require('../lib/logging.js');
20+
const LoggingService = require('../lib/logging_service.js');
21+
22+
const LOGGING_SEVERITY = {
23+
UNSET: 0,
24+
DEBUG: 10,
25+
INFO: 20,
26+
};
27+
28+
describe('LoggingService test suite', function () {
29+
let sandbox;
30+
31+
beforeEach(function () {
32+
sandbox = sinon.createSandbox();
33+
});
34+
35+
afterEach(function () {
36+
sandbox.restore();
37+
});
38+
39+
it('starts get_logger_levels and set_logger_levels services', function () {
40+
const node = {
41+
name: () => 'logger_node',
42+
createService: sandbox.stub(),
43+
};
44+
const service = new LoggingService(node);
45+
46+
service.start();
47+
service.start();
48+
49+
assert.strictEqual(service.isStarted(), true);
50+
assert.strictEqual(node.createService.callCount, 2);
51+
assert.deepStrictEqual(node.createService.firstCall.args.slice(0, 2), [
52+
'rcl_interfaces/srv/GetLoggerLevels',
53+
'logger_node/get_logger_levels',
54+
]);
55+
assert.deepStrictEqual(node.createService.secondCall.args.slice(0, 2), [
56+
'rcl_interfaces/srv/SetLoggerLevels',
57+
'logger_node/set_logger_levels',
58+
]);
59+
});
60+
61+
it('returns logger levels and maps lookup failures to UNSET', function () {
62+
sandbox.stub(Logging, 'getLogger').callsFake((name) => {
63+
if (name === 'missing_logger') {
64+
throw new Error('logger not found');
65+
}
66+
return { loggerEffectiveLevel: LOGGING_SEVERITY.INFO };
67+
});
68+
const service = new LoggingService({});
69+
const response = {
70+
template: { levels: [] },
71+
send: sandbox.spy(),
72+
};
73+
74+
service._handleGetLoggerLevels(
75+
{ names: ['existing_logger', 'missing_logger'] },
76+
response
77+
);
78+
79+
assert.strictEqual(response.send.calledOnce, true);
80+
assert.deepStrictEqual(response.send.firstCall.args[0].levels, [
81+
{ name: 'existing_logger', level: LOGGING_SEVERITY.INFO },
82+
{ name: 'missing_logger', level: LOGGING_SEVERITY.UNSET },
83+
]);
84+
});
85+
86+
it('sets logger levels and reports per-level failures', function () {
87+
sandbox.stub(Logging, 'getLogger').callsFake((name) => ({
88+
setLoggerLevel(level) {
89+
if (name === 'bad_logger') {
90+
throw new Error(`failed to set ${level}`);
91+
}
92+
},
93+
}));
94+
const service = new LoggingService({});
95+
const response = {
96+
template: { results: [] },
97+
send: sandbox.spy(),
98+
};
99+
100+
service._handleSetLoggerLevels(
101+
{
102+
levels: [
103+
{ name: 'good_logger', level: LOGGING_SEVERITY.DEBUG },
104+
{ name: 'bad_logger', level: 999 },
105+
],
106+
},
107+
response
108+
);
109+
110+
assert.strictEqual(response.send.calledOnce, true);
111+
assert.deepStrictEqual(response.send.firstCall.args[0].results, [
112+
{ successful: true, reason: '' },
113+
{ successful: false, reason: 'failed to set 999' },
114+
]);
115+
});
116+
});

test/test-node-options.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ describe('rclnodejs NodeOptions test suite', function () {
3535
nodeOptions.automaticallyDeclareParametersFromOverrides,
3636
false
3737
);
38+
assert.strictEqual(nodeOptions.enableLoggerService, false);
3839
assert.ok(Array.isArray(nodeOptions.parameterOverrides));
3940
assert.strictEqual(nodeOptions.parameterOverrides.length, 0);
4041
});
@@ -47,6 +48,7 @@ describe('rclnodejs NodeOptions test suite', function () {
4748
nodeOptions.automaticallyDeclareParametersFromOverrides,
4849
false
4950
);
51+
assert.strictEqual(nodeOptions.enableLoggerService, false);
5052
assert.ok(Array.isArray(nodeOptions.parameterOverrides));
5153
assert.strictEqual(nodeOptions.parameterOverrides.length, 0);
5254
});
@@ -61,13 +63,15 @@ describe('rclnodejs NodeOptions test suite', function () {
6163

6264
nodeOptions.startParameterServices = false;
6365
nodeOptions.automaticallyDeclareParametersFromOverrides = true;
66+
nodeOptions.enableLoggerService = true;
6467
nodeOptions.parameterOverrides = param;
6568

6669
assert.strictEqual(nodeOptions.startParameterServices, false);
6770
assert.strictEqual(
6871
nodeOptions.automaticallyDeclareParametersFromOverrides,
6972
true
7073
);
74+
assert.strictEqual(nodeOptions.enableLoggerService, true);
7175
assert.ok(Array.isArray(nodeOptions.parameterOverrides));
7276
assert.strictEqual(nodeOptions.parameterOverrides.length, 1);
7377
assert.strictEqual(nodeOptions.parameterOverrides[0].name, 'str_param');

test/types/index.test-d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ expectType<boolean>(nodeOptions.automaticallyDeclareParametersFromOverrides);
6464
expectType<rclnodejs.Parameter[]>(nodeOptions.parameterOverrides);
6565
expectType<boolean>(nodeOptions.enableRosout);
6666
expectType<rclnodejs.QoS | rclnodejs.QoS.ProfileRef>(nodeOptions.rosoutQos);
67+
expectType<boolean>(nodeOptions.enableLoggerService);
6768

6869
// ---- Node -----
6970
const node = rclnodejs.createNode(NODE_NAME);

types/node_options.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ declare module 'rclnodejs' {
4545
*/
4646
rosoutQos: QoS | QoS.ProfileRef;
4747

48+
/**
49+
* A flag controlling the startup of logger services.
50+
* When true a node will start get_logger_levels and set_logger_levels services.
51+
* Default value = false;
52+
*/
53+
enableLoggerService: boolean;
54+
4855
/**
4956
* An instance configured with default values.
5057
*/

0 commit comments

Comments
 (0)