Skip to content

Commit f50308f

Browse files
committed
feat: add configurable SDK_LOG_LEVEL for the SDKClient
Signed-off-by: Logan Nguyen <logan.nguyen@swirldslabs.com>
1 parent fc9d456 commit f50308f

File tree

3 files changed

+141
-0
lines changed

3 files changed

+141
-0
lines changed

packages/config-service/src/services/globalConfig.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,11 @@ const _CONFIG = {
368368
required: false,
369369
defaultValue: 'trace',
370370
},
371+
SDK_LOG_LEVEL: {
372+
type: 'string',
373+
required: false,
374+
defaultValue: 'trace',
375+
},
371376
MAX_BLOCK_RANGE: {
372377
type: 'number',
373378
required: false,

packages/relay/src/lib/clients/sdkClient.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
FileInfoQuery,
1515
Hbar,
1616
HbarUnit,
17+
Logger as HederaLogger,
18+
LogLevel,
1719
PublicKey,
1820
Query,
1921
Status,
@@ -90,6 +92,11 @@ export class SDKClient {
9092
const SDK_REQUEST_TIMEOUT = ConfigService.get('SDK_REQUEST_TIMEOUT');
9193
client.setRequestTimeout(SDK_REQUEST_TIMEOUT);
9294

95+
// Set up SDK logger with child configuration - direct and straightforward
96+
const sdkLogger = new HederaLogger(LogLevel._fromString(ConfigService.get('SDK_LOG_LEVEL')));
97+
sdkLogger.setLogger(logger.child({ name: 'sdk-client' }, { level: ConfigService.get('SDK_LOG_LEVEL') }));
98+
client.setLogger(sdkLogger);
99+
93100
logger.info(
94101
`SDK client successfully configured to ${JSON.stringify(hederaNetwork)} for account ${
95102
client.operatorAccountId

packages/relay/tests/lib/sdkClient.spec.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import assert from 'node:assert';
55
import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services';
66
import {
77
AccountId,
8+
Client,
89
EthereumTransaction,
910
ExchangeRate,
1011
FileAppendTransaction,
@@ -13,6 +14,8 @@ import {
1314
FileId,
1415
FileInfoQuery,
1516
Hbar,
17+
Logger as HederaLogger,
18+
LogLevel,
1619
Query,
1720
Status,
1821
TransactionId,
@@ -197,6 +200,132 @@ describe('SdkClient', async function () {
197200
});
198201
});
199202

203+
describe('SDK Logger Configuration', () => {
204+
// Simple helper to create standard mock logger
205+
const createMockLogger = (additionalProps = {}) => ({
206+
child: sinon.stub().returns({ name: 'sdk-client' }),
207+
info: sinon.stub(),
208+
...additionalProps,
209+
});
210+
211+
// Simple helper to create Client stub
212+
const createClientStub = () =>
213+
sinon.stub(Client, 'forName').returns({
214+
setOperator: sinon.stub().returnsThis(),
215+
setTransportSecurity: sinon.stub().returnsThis(),
216+
setRequestTimeout: sinon.stub().returnsThis(),
217+
setLogger: sinon.stub().returnsThis(),
218+
setMaxExecutionTime: sinon.stub().returnsThis(),
219+
operatorAccountId: null,
220+
operatorPublicKey: null,
221+
} as any);
222+
223+
// Simple helper to restore stubs
224+
const cleanupStubs = (...stubs: sinon.SinonStub[]) => {
225+
stubs.forEach((stub) => stub.restore());
226+
};
227+
228+
it('should create child logger with "sdk-client" name using SDK_LOG_LEVEL', () => {
229+
const mockLogger = createMockLogger();
230+
231+
const configStub = sinon.stub(ConfigService, 'get');
232+
configStub.withArgs('SDK_LOG_LEVEL').returns('debug');
233+
configStub.callThrough();
234+
235+
const clientStub = createClientStub();
236+
237+
try {
238+
const eventEmitter = new EventEmitter<TypedEvents>();
239+
new SDKClient('testnet', mockLogger as any, eventEmitter, hbarLimitService);
240+
241+
expect(mockLogger.child.calledWith({ name: 'sdk-client' }, { level: 'debug' })).to.be.true;
242+
} finally {
243+
cleanupStubs(configStub, clientStub);
244+
}
245+
});
246+
247+
it('should use SDK_LOG_LEVEL independently from global LOG_LEVEL', () => {
248+
const mockLogger = createMockLogger({ level: 'error' });
249+
250+
const configStub = sinon.stub(ConfigService, 'get');
251+
configStub.withArgs('SDK_LOG_LEVEL').returns('info');
252+
configStub.withArgs('LOG_LEVEL').returns('error');
253+
configStub.callThrough();
254+
255+
const clientStub = createClientStub();
256+
257+
try {
258+
const eventEmitter = new EventEmitter<TypedEvents>();
259+
new SDKClient('testnet', mockLogger as any, eventEmitter, hbarLimitService);
260+
261+
// Verify SDK logger uses SDK_LOG_LEVEL ('info'), not global LOG_LEVEL ('error')
262+
expect(mockLogger.child.calledWith({ name: 'sdk-client' }, { level: 'info' })).to.be.true;
263+
} finally {
264+
cleanupStubs(configStub, clientStub);
265+
}
266+
});
267+
268+
it('should create child logger that inherits from global logger with SDK log level override', () => {
269+
const mockGlobalLogger = createMockLogger({
270+
level: 'trace',
271+
service: 'hedera-relay', // Global logger has this binding
272+
});
273+
274+
// Mock child() to simulate pino inheritance behavior
275+
mockGlobalLogger.child.callsFake((newBindings, options) => ({
276+
service: 'hedera-relay', // Inherited from parent
277+
name: newBindings.name, // Added by child
278+
level: options.level, // Overridden by child options
279+
}));
280+
281+
const configStub = sinon.stub(ConfigService, 'get');
282+
configStub.withArgs('SDK_LOG_LEVEL').returns('warn');
283+
configStub.callThrough();
284+
285+
const clientStub = createClientStub();
286+
287+
try {
288+
const eventEmitter = new EventEmitter<TypedEvents>();
289+
new SDKClient('testnet', mockGlobalLogger as any, eventEmitter, hbarLimitService);
290+
291+
// Verify child() was called correctly
292+
expect(mockGlobalLogger.child.calledWith({ name: 'sdk-client' }, { level: 'warn' })).to.be.true;
293+
294+
// Verify the child logger has the right properties
295+
const childLogger = mockGlobalLogger.child.returnValues[0];
296+
expect(childLogger.service).to.equal('hedera-relay'); // Inherited
297+
expect(childLogger.name).to.equal('sdk-client'); // Added by child
298+
expect(childLogger.level).to.equal('warn'); // Overridden level
299+
} finally {
300+
cleanupStubs(configStub, clientStub);
301+
}
302+
});
303+
304+
it('should work across different log levels', () => {
305+
const testLevels = ['trace', 'debug', 'info', 'warn', 'error'];
306+
307+
testLevels.forEach((level) => {
308+
const mockLogger = createMockLogger();
309+
310+
const configStub = sinon.stub(ConfigService, 'get');
311+
configStub.withArgs('SDK_LOG_LEVEL').returns(level);
312+
configStub.callThrough();
313+
314+
const clientStub = createClientStub();
315+
316+
try {
317+
const eventEmitter = new EventEmitter<TypedEvents>();
318+
new SDKClient('testnet', mockLogger as any, eventEmitter, hbarLimitService);
319+
320+
// Verify child logger is always created with 'sdk-client' name
321+
expect(mockLogger.child.calledWith({ name: 'sdk-client' }, { level })).to.be.true;
322+
} finally {
323+
cleanupStubs(configStub, clientStub);
324+
}
325+
});
326+
});
327+
});
328+
200329
describe('submitEthereumTransaction', () => {
201330
const accountId = AccountId.fromString('0.0.1234');
202331
const chunkSize = ConfigService.get('FILE_APPEND_CHUNK_SIZE');

0 commit comments

Comments
 (0)