Skip to content

Commit

Permalink
Add correlation ID to all use cases and send it to backbone (#268)
Browse files Browse the repository at this point in the history
* feat: add correlation id to all RESTClient calls

* chore: add test for correlation id

* test: add test

* test: cleanup tests

* Update packages/transport/test/core/backbone/CorrelationId.test.ts

Co-authored-by: Julian König <33655937+jkoenig134@users.noreply.github.com>

* Update packages/transport/test/core/backbone/CorrelationId.test.ts

Co-authored-by: Julian König <33655937+jkoenig134@users.noreply.github.com>

* chore: typo

* feat: add correlation id to all useCases

* refactor: rename correlationIdLib to correlator

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Julian König <33655937+jkoenig134@users.noreply.github.com>
Co-authored-by: Julian König <julian.koenig@js-soft.com>
  • Loading branch information
4 people committed Sep 13, 2024
1 parent e6aebf7 commit 8510f91
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 11 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 17 additions & 10 deletions packages/runtime/src/useCases/common/UseCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ParsingError, ServalError, ValidationError } from "@js-soft/ts-serval";
import { ApplicationError, Result } from "@js-soft/ts-utils";
import { CoreError } from "@nmshd/core-types";
import { RequestError } from "@nmshd/transport";
import correlator from "correlation-id";
import stringifySafe from "json-stringify-safe";
import { PlatformErrorCodes } from "./PlatformErrorCodes";
import { RuntimeErrors } from "./RuntimeErrors";
Expand All @@ -12,19 +13,25 @@ export abstract class UseCase<IRequest, IResponse> {
public constructor(private readonly requestValidator?: IValidator<IRequest>) {}

public async execute(request: IRequest): Promise<Result<IResponse>> {
if (this.requestValidator) {
const validationResult = await this.requestValidator.validate(request);
const callback = async (): Promise<Result<IResponse>> => {
if (this.requestValidator) {
const validationResult = await this.requestValidator.validate(request);

if (validationResult.isInvalid()) {
return this.validationFailed(validationResult);
if (validationResult.isInvalid()) {
return this.validationFailed(validationResult);
}
}
}

try {
return await this.executeInternal(request);
} catch (e) {
return this.failingResultFromUnknownError(e);
}
try {
return await this.executeInternal(request);
} catch (e) {
return this.failingResultFromUnknownError(e);
}
};

const correlationId = correlator.getId();
if (correlationId) return await correlator.withId(correlationId, callback);
return await correlator.withId(callback);
}

private failingResultFromUnknownError(error: unknown): Result<any> {
Expand Down
73 changes: 73 additions & 0 deletions packages/runtime/test/lib/RequestInterceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { RESTClient } from "@nmshd/transport";
import { AxiosRequestConfig, AxiosResponse, Method } from "axios";

export class RequestInterceptor {
protected _measuringRequests = true;
public get measuringRequests(): boolean {
return this._measuringRequests;
}

protected _requests: AxiosRequestConfig[] = [];
public get requests(): AxiosRequestConfig[] {
return this._requests;
}

protected _responses: AxiosResponse[] = [];
public get responses(): AxiosResponse[] {
return this._responses;
}

protected _client: RESTClient;
public get controller(): RESTClient {
return this._client;
}

public constructor(client: RESTClient) {
this._client = client;
this._measuringRequests = true;
this.injectToClient(client);
}

private injectToClient(client: RESTClient) {
const that = this;

const axiosInstance = client["axiosInstance"];
axiosInstance.interceptors.request.use((req) => {
if (!that._measuringRequests) return req;
that._requests.push(req);
return req;
});
axiosInstance.interceptors.response.use((res) => {
if (!that._measuringRequests) return res;
that._responses.push(res);
return res;
});
}

public start(): this {
this._measuringRequests = true;
this.reset();
return this;
}

private reset() {
this._requests = [];
this._responses = [];
}

public stop(): Communication {
this._measuringRequests = false;
return new Communication(this.requests, this.responses);
}
}

class Communication {
public constructor(
public readonly requests: AxiosRequestConfig[],
public readonly responses: AxiosResponse<any>[]
) {}

public getRequests(filter: { method: Method; urlSubstring: string }) {
return this.requests.filter((r) => r.url!.toLowerCase().includes(filter.urlSubstring.toLowerCase()) && r.method?.toLowerCase() === filter.method.toLowerCase());
}
}
44 changes: 44 additions & 0 deletions packages/runtime/test/misc/CorrelationId.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { AccountController } from "@nmshd/transport";
import correlator from "correlation-id";
import { Container } from "typescript-ioc";
import { RuntimeServiceProvider, TestRuntimeServices } from "../lib";
import { RequestInterceptor } from "../lib/RequestInterceptor";

const uuidRegex = new RegExp("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}");

describe("CorrelationId", function () {
let runtime: TestRuntimeServices;
let runtimeServiceProvider: RuntimeServiceProvider;
let interceptor: RequestInterceptor;

beforeAll(async function () {
runtimeServiceProvider = new RuntimeServiceProvider();
runtime = (await runtimeServiceProvider.launch(1))[0];

const accountController = Container.get(AccountController);
interceptor = new RequestInterceptor((accountController as any).synchronization.client);
});

afterAll(async function () {
await runtimeServiceProvider.stop();
});

test("should send correlation id to the backbone when given", async function () {
interceptor.start();
await correlator.withId("test-correlation-id", async () => {
await runtime.transport.account.syncEverything();
});

const requests = interceptor.stop().requests;
expect(requests.at(-1)!.headers!["x-correlation-id"]).toBe("test-correlation-id");
});

test("should send a generated correlation id to the backbone", async function () {
interceptor.start();

await runtime.transport.account.syncEverything();

const requests = interceptor.stop().requests;
expect(requests.at(-1)!.headers!["x-correlation-id"]).toMatch(uuidRegex);
});
});
1 change: 1 addition & 0 deletions packages/transport/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"@nmshd/core-types": "*",
"@nmshd/crypto": "2.0.6",
"axios": "^1.7.7",
"correlation-id": "^5.2.0",
"fast-json-patch": "^3.1.1",
"form-data": "^4.0.0",
"https-proxy-agent": "^7.0.5",
Expand Down
7 changes: 7 additions & 0 deletions packages/transport/src/core/backbone/RESTClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ILogger } from "@js-soft/logging-abstractions";
import { CoreBuffer } from "@nmshd/crypto";
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
import correlator from "correlation-id";
import formDataLib from "form-data";
import { AgentOptions } from "http";
import { AgentOptions as HTTPSAgentOptions } from "https";
Expand Down Expand Up @@ -114,6 +115,12 @@ export class RESTClient {
this._logger = TransportLoggerFactory.getLogger(RESTClient);

this.axiosInstance = axios.create(resultingRequestConfig);
this.axiosInstance.interceptors.request.use((config) => {
const correlationId = correlator.getId();
config.headers["x-correlation-id"] = correlationId;
return config;
});

if (this.config.debug) {
this.addAxiosLoggingInterceptors(this.axiosInstance);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ describe("AuthenticationTest", function () {
testAccount = accounts[0];
interceptor = new RequestInterceptor((testAccount.authenticator as any).authClient);
});

afterAll(async function () {
await testAccount.close();
await connection.close();
Expand Down
35 changes: 35 additions & 0 deletions packages/transport/test/core/backbone/CorrelationId.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions";
import correlator from "correlation-id";
import { AccountController } from "../../../src";
import { RequestInterceptor } from "../../testHelpers/RequestInterceptor";
import { TestUtil } from "../../testHelpers/TestUtil";

describe("CorrelationId", function () {
let connection: IDatabaseConnection;
let testAccount: AccountController;
let interceptor: RequestInterceptor;

beforeAll(async function () {
connection = await TestUtil.createDatabaseConnection();
const transport = TestUtil.createTransport(connection);
await transport.init();
const accounts = await TestUtil.provideAccounts(transport, 1);
testAccount = accounts[0];
interceptor = new RequestInterceptor((testAccount as any).synchronization.client);
});

afterAll(async function () {
await testAccount.close();
await connection.close();
});

test("should send correlation id to the backbone when given", async function () {
interceptor.start();
await correlator.withId("test-correlation-id", async () => {
await testAccount.syncEverything();
});

const requests = interceptor.stop().requests;
expect(requests.at(-1)!.headers!["x-correlation-id"]).toBe("test-correlation-id");
});
});

0 comments on commit 8510f91

Please sign in to comment.