feat: add logging

This commit is contained in:
2026-08-14 17:12:15 +03:00
parent 34c2b79e27
commit 25d7c5780b
25 changed files with 753 additions and 108 deletions
+56 -46
View File
@@ -1,8 +1,4 @@
import {
Injectable,
Logger,
ServiceUnavailableException,
} from '@nestjs/common';
import { Injectable, ServiceUnavailableException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
ChannelCredentials,
@@ -17,6 +13,7 @@ import { loadSync } from '@grpc/proto-loader';
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { Environment } from '../config/env.validation';
import { StructuredLogger } from '../common/logging/structured-logger.service';
const requestDeadlineMs = 15_000;
@@ -129,13 +126,15 @@ class RateLimiter {
@Injectable()
export class TbankClient {
private readonly logger = new Logger(TbankClient.name);
private readonly queue: RateLimiter;
private readonly users: TbankClientService;
private readonly operations: TbankClientService;
private readonly instruments: TbankClientService;
constructor(config: ConfigService<Environment, true>) {
constructor(
config: ConfigService<Environment, true>,
private readonly logger: StructuredLogger,
) {
const sourceProtoRoot = join(process.cwd(), 'src/tbank/proto/contracts');
const protoRoot = existsSync(sourceProtoRoot)
? sourceProtoRoot
@@ -174,14 +173,7 @@ export class TbankClient {
this.queue = new RateLimiter(
config.getOrThrow<number>('TBANK_RATE_LIMIT_PER_SECOND'),
);
this.logger.log({
event: 'client_configured',
endpoint,
rateLimitPerSecond: config.getOrThrow<number>(
'TBANK_RATE_LIMIT_PER_SECOND',
),
customCa: Boolean(caPath),
});
this.logger.info('tbank.client.configured');
}
async validateToken(token: string): Promise<void> {
@@ -242,41 +234,59 @@ export class TbankClient {
client: TbankClientService,
method: string,
request: object,
retry = true,
): Promise<Response> {
if (this.queue.size >= 50)
throw new ServiceUnavailableException({ code: 'TBANK_UNAVAILABLE' });
try {
return await this.queue.run(() =>
this.request<Response>(token, client, method, request),
);
} catch (error) {
const code = (error as ServiceError).code;
const retryable = [
status.UNAVAILABLE,
status.DEADLINE_EXCEEDED,
status.RESOURCE_EXHAUSTED,
].includes(code);
this.logger.warn({
event: 'tbank_rpc_failed',
method,
grpcCode: code,
retryable,
...(method === 'getInstrumentBy'
? {
instrumentIdType: (request as { idType?: string }).idType,
instrumentId: (request as { id?: string }).id,
}
: {}),
if (this.queue.size >= 50) {
this.logger.event('error', 'tbank.rpc.failed', {
operation: method,
code: 'TBANK_UNAVAILABLE',
attempts: 0,
});
if (retry && retryable) {
await new Promise((resolve) => setTimeout(resolve, 1000));
return this.call(token, client, method, request, false);
}
if ([status.UNAUTHENTICATED, status.PERMISSION_DENIED].includes(code))
throw new Error('INVALID_TBANK_TOKEN');
throw new ServiceUnavailableException({ code: 'TBANK_UNAVAILABLE' });
}
for (let attempts = 1; attempts <= 2; attempts += 1) {
const startedAt = performance.now();
try {
const response = await this.queue.run(() =>
this.request<Response>(token, client, method, request),
);
this.logger.event('debug', 'tbank.rpc.completed', {
operation: method,
attempts,
durationMs: Math.round(performance.now() - startedAt),
});
return response;
} catch (error) {
const grpcCode = (error as ServiceError).code;
const retryable = [
status.UNAVAILABLE,
status.DEADLINE_EXCEEDED,
status.RESOURCE_EXHAUSTED,
].includes(grpcCode);
if (retryable && attempts < 2) {
this.logger.event('warn', 'tbank.rpc.retry', {
operation: method,
grpcCode,
attempts,
durationMs: Math.round(performance.now() - startedAt),
});
await new Promise((resolve) => setTimeout(resolve, 1000));
continue;
}
this.logger.event('error', 'tbank.rpc.failed', {
operation: method,
grpcCode,
attempts,
durationMs: Math.round(performance.now() - startedAt),
});
if (
[status.UNAUTHENTICATED, status.PERMISSION_DENIED].includes(grpcCode)
)
throw new Error('INVALID_TBANK_TOKEN');
throw new ServiceUnavailableException({ code: 'TBANK_UNAVAILABLE' });
}
}
throw new ServiceUnavailableException({ code: 'TBANK_UNAVAILABLE' });
}
private request<Response>(