diff --git a/docs/configuration.md b/docs/configuration.md index e82558f51a..534502d866 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -111,26 +111,27 @@ Unless you need to set a non-default value, it is recommended to only populate o The following table lists the available properties along with their default values for the [Server package](/packages/server/). Unless you need to set a non-default value, it is recommended to only populate overridden properties in the custom `.env`. -| Name | Default | Description | -| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `BATCH_REQUESTS_DISALLOWED_METHODS` | ["debug_traceTransaction", "debug_traceBlockByNumber", "eth_newFilter", "eth_uninstallFilter", "eth_getFilterChanges", "eth_getFilterLogs", "eth_newBlockFilter", "eth_newPendingTransactionFilter"] | A list of the methods that are not allowed for batch. | -| `BATCH_REQUESTS_ENABLED` | "true" | Flag to disable or enable batch requests. | -| `BATCH_REQUESTS_MAX_SIZE` | "100" | Maximum number of requests allowed in a batch. | -| `CHAIN_ID` | "" | The network chain id. Local and previewnet envs should use `0x12a` (298). Previewnet, Testnet and Mainnet should use `0x129` (297), `0x128` (296) and `0x127` (295) respectively. | -| `DISABLE_ADMIN_NAMESPACE` | "false" | The JSON-RPC Relay provide custom methods under the `admin_` namespace. This flag enables or disables access to this namespace. Enabled by default. | -| `HEDERA_NETWORK` | "" | Which network to connect to. Automatically populates the main node & mirror node endpoints. Can be `previewnet`, `testnet`, `mainnet` or a map of network IPs -> node accountIds e.g. `{"127.0.0.1:50211":"0.0.3"}` | -| `INPUT_SIZE_LIMIT` | "1mb" | The [koa-jsonrpc](https://github.com/Bitclimb/koa-jsonrpc) maximum size allowed for requests | -| `LOG_LEVEL` | "trace" | The logging level for the application. Valid values are `trace`, `debug`, `info`, `warn`, `error`, and `fatal`. | -| `PRETTY_LOGS_ENABLED` | "true" | Controls the logging output format. When set to `true` (default), uses human-readable pretty format with colors. When set to `false`, uses structured JSON format for log aggregation systems. | -| `MAX_BLOCK_RANGE` | "5" | The maximum block number greater than the mirror node's latest block to query for | -| `OPERATOR_ID_MAIN` | "" | Operator account ID used to pay for transactions. In `S.R.N` format, e.g. `0.0.1001`. | -| `OPERATOR_KEY_FORMAT` | "DER" | Operator private key format. Valid types are `DER`, `HEX_ECDSA`, or `HEX_ED25519` | -| `OPERATOR_KEY_MAIN` | "" | Operator private key used to sign transactions in hex encoded DER format. This may be either an ED22519 private key or an ECDSA private key. The private key must be associated with/used to derive `OPERATOR_ID_MAIN`. | -| `RATE_LIMIT_DISABLED` | "false" | Flag to disable IP based rate limiting. | -| `REQUEST_ID_IS_OPTIONAL` | "false" | Flag to set it the JSON RPC request id field in the body should be optional. Note, this breaks the API spec and is not advised and is provided for test purposes only where some wallets may be non compliant | -| `SERVER_HOST` | undefined | The hostname or IP address on which the server listens for incoming connections. If `SERVER_HOST` is not configured or left undefined (same as `0.0.0.0`), it permits external connections by default, offering more flexibility. | -| `SERVER_PORT` | "7546" | The RPC server port number to listen for requests on. Currently a static value defaulting to 7546. See [#955](https://github.com/hiero-ledger/hiero-json-rpc-relay/issues/955) | -| `SERVER_REQUEST_TIMEOUT_MS` | "60000" | The time of inactivity allowed before a timeout is triggered and the socket is closed. See [NodeJs Server Timeout](https://nodejs.org/api/http.html#serversettimeoutmsecs-callback) | +| Name | Default | Description | +| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `BATCH_REQUESTS_DISALLOWED_METHODS` | ["debug_traceTransaction", "debug_traceBlockByNumber", "eth_newFilter", "eth_uninstallFilter", "eth_getFilterChanges", "eth_getFilterLogs", "eth_newBlockFilter", "eth_newPendingTransactionFilter"] | A list of the methods that are not allowed for batch. | +| `BATCH_REQUESTS_ENABLED` | "true" | Flag to disable or enable batch requests. | +| `BATCH_REQUESTS_MAX_SIZE` | "100" | Maximum number of requests allowed in a batch. | +| `CHAIN_ID` | "" | The network chain id. Local and previewnet envs should use `0x12a` (298). Previewnet, Testnet and Mainnet should use `0x129` (297), `0x128` (296) and `0x127` (295) respectively. | +| `DISABLE_ADMIN_NAMESPACE` | "false" | The JSON-RPC Relay provide custom methods under the `admin_` namespace. This flag enables or disables access to this namespace. Enabled by default. | +| `HEDERA_NETWORK` | "" | Which network to connect to. Automatically populates the main node & mirror node endpoints. Can be `previewnet`, `testnet`, `mainnet` or a map of network IPs -> node accountIds e.g. `{"127.0.0.1:50211":"0.0.3"}` | +| `INPUT_SIZE_LIMIT` | "1mb" | The [koa-jsonrpc](https://github.com/Bitclimb/koa-jsonrpc) maximum size allowed for requests | +| `LOG_LEVEL` | "trace" | The logging level for the application. Valid values are `trace`, `debug`, `info`, `warn`, `error`, and `fatal`. | +| `PRETTY_LOGS_ENABLED` | "true" | Controls the logging output format. When set to `true` (default), uses human-readable pretty format with colors. When set to `false`, uses structured JSON format for log aggregation systems. | +| `MAX_BLOCK_RANGE` | "5" | The maximum block number greater than the mirror node's latest block to query for | +| `OPERATOR_ID_MAIN` | "" | Operator account ID used to pay for transactions. In `S.R.N` format, e.g. `0.0.1001`. | +| `OPERATOR_KEY_FORMAT` | "DER" | Operator private key format. Valid types are `DER`, `HEX_ECDSA`, or `HEX_ED25519` | +| `OPERATOR_KEY_MAIN` | "" | Operator private key used to sign transactions in hex encoded DER format. This may be either an ED22519 private key or an ECDSA private key. The private key must be associated with/used to derive `OPERATOR_ID_MAIN`. | +| `RATE_LIMIT_DISABLED` | "false" | Flag to disable IP based rate limiting. | +| `REQUEST_ID_IS_OPTIONAL` | "false" | Flag to set it the JSON RPC request id field in the body should be optional. Note, this breaks the API spec and is not advised and is provided for test purposes only where some wallets may be non compliant | +| `SDK_LOG_LEVEL` | "silent" | The logging level for the Hedera SDK, independent from the application's `LOG_LEVEL` setting. This allows fine-tuned debugging of SDK operations without affecting the main application logs. Valid values are `silent`, `trace`, `debug`, `info`, `warn`, `error`, and `fatal`. | +| `SERVER_HOST` | undefined | The hostname or IP address on which the server listens for incoming connections. If `SERVER_HOST` is not configured or left undefined (same as `0.0.0.0`), it permits external connections by default, offering more flexibility. | +| `SERVER_PORT` | "7546" | The RPC server port number to listen for requests on. Currently a static value defaulting to 7546. See [#955](https://github.com/hiero-ledger/hiero-json-rpc-relay/issues/955) | +| `SERVER_REQUEST_TIMEOUT_MS` | "60000" | The time of inactivity allowed before a timeout is triggered and the socket is closed. See [NodeJs Server Timeout](https://nodejs.org/api/http.html#serversettimeoutmsecs-callback) | ## WS-Server diff --git a/packages/config-service/src/services/globalConfig.ts b/packages/config-service/src/services/globalConfig.ts index 48dcf367d8..a61d46a94e 100644 --- a/packages/config-service/src/services/globalConfig.ts +++ b/packages/config-service/src/services/globalConfig.ts @@ -559,6 +559,11 @@ const _CONFIG = { required: false, defaultValue: false, }, + SDK_LOG_LEVEL: { + type: 'string', + required: false, + defaultValue: 'silent', + }, SDK_REQUEST_TIMEOUT: { type: 'number', required: false, diff --git a/packages/relay/src/lib/clients/sdkClient.ts b/packages/relay/src/lib/clients/sdkClient.ts index 94ebcf6fc3..d19884db32 100644 --- a/packages/relay/src/lib/clients/sdkClient.ts +++ b/packages/relay/src/lib/clients/sdkClient.ts @@ -14,6 +14,8 @@ import { FileInfoQuery, Hbar, HbarUnit, + Logger as HederaLogger, + LogLevel, PublicKey, Query, Status, @@ -90,6 +92,11 @@ export class SDKClient { const SDK_REQUEST_TIMEOUT = ConfigService.get('SDK_REQUEST_TIMEOUT'); client.setRequestTimeout(SDK_REQUEST_TIMEOUT); + // Set up SDK logger with child configuration inheriting from the main logger + const sdkLogger = new HederaLogger(LogLevel._fromString(ConfigService.get('SDK_LOG_LEVEL'))); + sdkLogger.setLogger(logger.child({ name: 'sdk-client' }, { level: ConfigService.get('SDK_LOG_LEVEL') })); + client.setLogger(sdkLogger); + logger.info( `SDK client successfully configured to ${JSON.stringify(hederaNetwork)} for account ${ client.operatorAccountId diff --git a/packages/relay/tests/lib/sdkClient.spec.ts b/packages/relay/tests/lib/sdkClient.spec.ts index e00b7807a7..b82781391c 100644 --- a/packages/relay/tests/lib/sdkClient.spec.ts +++ b/packages/relay/tests/lib/sdkClient.spec.ts @@ -5,6 +5,7 @@ import assert from 'node:assert'; import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services'; import { AccountId, + Client, EthereumTransaction, ExchangeRate, FileAppendTransaction, @@ -13,6 +14,8 @@ import { FileId, FileInfoQuery, Hbar, + Logger as HederaLogger, + LogLevel, Query, Status, TransactionId, @@ -197,6 +200,132 @@ describe('SdkClient', async function () { }); }); + describe('SDK Logger Configuration', () => { + // Simple helper to create standard mock logger + const createMockLogger = (additionalProps = {}) => ({ + child: sinon.stub().returns({ name: 'sdk-client' }), + info: sinon.stub(), + ...additionalProps, + }); + + // Simple helper to create Client stub + const createClientStub = () => + sinon.stub(Client, 'forName').returns({ + setOperator: sinon.stub().returnsThis(), + setTransportSecurity: sinon.stub().returnsThis(), + setRequestTimeout: sinon.stub().returnsThis(), + setLogger: sinon.stub().returnsThis(), + setMaxExecutionTime: sinon.stub().returnsThis(), + operatorAccountId: null, + operatorPublicKey: null, + } as any); + + // Simple helper to restore stubs + const cleanupStubs = (...stubs: sinon.SinonStub[]) => { + stubs.forEach((stub) => stub.restore()); + }; + + it('should create child logger with "sdk-client" name using SDK_LOG_LEVEL', () => { + const mockLogger = createMockLogger(); + + const configStub = sinon.stub(ConfigService, 'get'); + configStub.withArgs('SDK_LOG_LEVEL').returns('debug'); + configStub.callThrough(); + + const clientStub = createClientStub(); + + try { + const eventEmitter = new EventEmitter(); + new SDKClient('testnet', mockLogger as any, eventEmitter, hbarLimitService); + + expect(mockLogger.child.calledWith({ name: 'sdk-client' }, { level: 'debug' })).to.be.true; + } finally { + cleanupStubs(configStub, clientStub); + } + }); + + it('should use SDK_LOG_LEVEL independently from global LOG_LEVEL', () => { + const mockLogger = createMockLogger({ level: 'error' }); + + const configStub = sinon.stub(ConfigService, 'get'); + configStub.withArgs('SDK_LOG_LEVEL').returns('info'); + configStub.withArgs('LOG_LEVEL').returns('error'); + configStub.callThrough(); + + const clientStub = createClientStub(); + + try { + const eventEmitter = new EventEmitter(); + new SDKClient('testnet', mockLogger as any, eventEmitter, hbarLimitService); + + // Verify SDK logger uses SDK_LOG_LEVEL ('info'), not global LOG_LEVEL ('error') + expect(mockLogger.child.calledWith({ name: 'sdk-client' }, { level: 'info' })).to.be.true; + } finally { + cleanupStubs(configStub, clientStub); + } + }); + + it('should create child logger that inherits from global logger with SDK log level override', () => { + const mockGlobalLogger = createMockLogger({ + level: 'trace', + service: 'hedera-relay', // Global logger has this binding + }); + + // Mock child() to simulate pino inheritance behavior + mockGlobalLogger.child.callsFake((newBindings, options) => ({ + service: 'hedera-relay', // Inherited from parent + name: newBindings.name, // Added by child + level: options.level, // Overridden by child options + })); + + const configStub = sinon.stub(ConfigService, 'get'); + configStub.withArgs('SDK_LOG_LEVEL').returns('warn'); + configStub.callThrough(); + + const clientStub = createClientStub(); + + try { + const eventEmitter = new EventEmitter(); + new SDKClient('testnet', mockGlobalLogger as any, eventEmitter, hbarLimitService); + + // Verify child() was called correctly + expect(mockGlobalLogger.child.calledWith({ name: 'sdk-client' }, { level: 'warn' })).to.be.true; + + // Verify the child logger has the right properties + const childLogger = mockGlobalLogger.child.returnValues[0]; + expect(childLogger.service).to.equal('hedera-relay'); // Inherited + expect(childLogger.name).to.equal('sdk-client'); // Added by child + expect(childLogger.level).to.equal('warn'); // Overridden level + } finally { + cleanupStubs(configStub, clientStub); + } + }); + + it('should work across different log levels', () => { + const testLevels = ['trace', 'debug', 'info', 'warn', 'error']; + + testLevels.forEach((level) => { + const mockLogger = createMockLogger(); + + const configStub = sinon.stub(ConfigService, 'get'); + configStub.withArgs('SDK_LOG_LEVEL').returns(level); + configStub.callThrough(); + + const clientStub = createClientStub(); + + try { + const eventEmitter = new EventEmitter(); + new SDKClient('testnet', mockLogger as any, eventEmitter, hbarLimitService); + + // Verify child logger is always created with 'sdk-client' name + expect(mockLogger.child.calledWith({ name: 'sdk-client' }, { level })).to.be.true; + } finally { + cleanupStubs(configStub, clientStub); + } + }); + }); + }); + describe('submitEthereumTransaction', () => { const accountId = AccountId.fromString('0.0.1234'); const chunkSize = ConfigService.get('FILE_APPEND_CHUNK_SIZE');