Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion src/subdomains/generic/gs/__tests__/gs.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { BadRequestException } from '@nestjs/common';
import { createMock } from '@golevelup/ts-jest';
import { DataSource } from 'typeorm';
import { AppInsightsQueryService } from 'src/integration/infrastructure/app-insights-query.service';
import { DfxLogger } from 'src/shared/services/dfx-logger';
import { GsService } from '../gs.service';
import { DebugLogQueryTemplates, LogQueryAuditPrefix } from '../dto/gs.dto';
import { LogQueryTemplate } from '../dto/log-query.dto';
import { UserDataService } from '../../user/models/user-data/user-data.service';
import { UserService } from '../../user/models/user/user.service';
import { BuyService } from 'src/subdomains/core/buy-crypto/routes/buy/buy.service';
Expand All @@ -27,12 +30,14 @@ import { VirtualIbanService } from 'src/subdomains/supporting/bank/virtual-iban/
describe('GsService', () => {
let service: GsService;
let dataSource: DataSource;
let appInsightsQueryService: AppInsightsQueryService;

beforeEach(() => {
dataSource = createMock<DataSource>();
appInsightsQueryService = createMock<AppInsightsQueryService>();

service = new GsService(
createMock<AppInsightsQueryService>(),
appInsightsQueryService,
createMock<UserDataService>(),
createMock<UserService>(),
createMock<BuyService>(),
Expand Down Expand Up @@ -192,4 +197,27 @@ describe('GsService', () => {
});
});
});

describe('LogQueryAuditPrefix sync', () => {
it('ALL_TRACES template excludes the exact audit prefix that gs.service emits', async () => {
const verboseSpy = jest.spyOn(DfxLogger.prototype, 'verbose').mockImplementation(() => undefined);
jest.spyOn(appInsightsQueryService, 'query').mockResolvedValue({ tables: [{ columns: [], rows: [] }] } as never);

await service.executeLogQuery({ template: LogQueryTemplate.ALL_TRACES, hours: 1 }, '0xtester');

// 1) The service emits an audit log that starts with LogQueryAuditPrefix
const emitted = verboseSpy.mock.calls.map((args) => String(args[0])).join('\n');
expect(emitted).toContain(`${LogQueryAuditPrefix}0xtester`);
expect(emitted.startsWith(LogQueryAuditPrefix)).toBe(true);

// 2) The ALL_TRACES template KQL excludes lines with that exact prefix
// (after DfxLogger's "[GsService] " class-context prefix). This binds
// service and template via the shared constant — refactoring the
// constant will update both sides at once.
const kql = DebugLogQueryTemplates[LogQueryTemplate.ALL_TRACES].kql;
expect(kql).toContain(`[GsService] ${LogQueryAuditPrefix}`);

verboseSpy.mockRestore();
});
});
});
28 changes: 28 additions & 0 deletions src/subdomains/generic/gs/dto/gs.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ export const GsRestrictedColumns: Record<string, string[]> = {
asset: ['ikna'],
};

/**
* Prefix of the verbose audit message emitted by `gs.service.executeLogQuery`
* (`[GsService] Log query by ...`). The ALL_TRACES template excludes lines
* with this prefix to prevent recursive self-match for high-frequency
* callers. Keep service and template in sync via this constant.
*
* Note: the leading `[GsService] ` is prepended by `DfxLogger` from the
* `GsService` class name, not from this constant. Renaming the service
* class would break the KQL filter silently — no test covers that path.
*/
export const LogQueryAuditPrefix = 'Log query by ';

// Debug endpoint
export const DebugMaxResults = 10000;
export const DebugBlockedSchemas = ['sys', 'information_schema', 'master', 'msdb', 'tempdb', 'pg_catalog', 'pg_toast'];
Expand Down Expand Up @@ -182,6 +194,22 @@ export const DebugLogQueryTemplates: Record<
requiredParams: ['eventName'],
defaultLimit: 500,
},
[LogQueryTemplate.ALL_TRACES]: {
// Returns all trace entries in the given window. Self-emitted audit lines
// from /gs/debug/logs (start with "[GsService] " + LogQueryAuditPrefix)
// are filtered out at the source so they don't crowd the result for
// high-frequency dashboard callers. The "[GsService] " prefix is added by
// DfxLogger's class-context; LogQueryAuditPrefix is the message prefix
// emitted by gs.service.executeLogQuery — both sides reference the same
// constant to keep service and template in sync.
kql: `traces
| where timestamp > ago({hours}h)
| where not(message startswith "[GsService] ${LogQueryAuditPrefix}")
| project timestamp, severityLevel, message, operation_Id
| order by timestamp desc`,
requiredParams: [],
defaultLimit: 500,
},
};

// Support endpoint
Expand Down
1 change: 1 addition & 0 deletions src/subdomains/generic/gs/dto/log-query.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export enum LogQueryTemplate {
REQUEST_FAILURES = 'request-failures',
DEPENDENCIES_SLOW = 'dependencies-slow',
CUSTOM_EVENTS = 'custom-events',
ALL_TRACES = 'all-traces',
}

export class LogQueryDto {
Expand Down
7 changes: 5 additions & 2 deletions src/subdomains/generic/gs/gs.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
DebugMaxResults,
GsRestrictedColumns,
GsRestrictedMarker,
LogQueryAuditPrefix,
SupportTable,
} from './dto/gs.dto';
import { LogQueryDto, LogQueryResult } from './dto/log-query.dto';
Expand Down Expand Up @@ -285,7 +286,9 @@ export class GsService {
kql += `\n| take ${template.defaultLimit}`;

// Log for audit
this.logger.verbose(`Log query by ${userIdentifier}: template=${dto.template}, params=${JSON.stringify(dto)}`);
this.logger.verbose(
`${LogQueryAuditPrefix}${userIdentifier}: template=${dto.template}, params=${JSON.stringify(dto)}`,
);

// Execute
const timespan = `PT${dto.hours ?? 1}H`;
Expand All @@ -302,7 +305,7 @@ export class GsService {
rows: response.tables[0].rows,
};
} catch (e) {
this.logger.info(`Log query by ${userIdentifier} failed: ${e.message}`);
this.logger.info(`${LogQueryAuditPrefix}${userIdentifier} failed: ${e.message}`);
throw new BadRequestException('Query execution failed');
}
}
Expand Down