Axios with OpenTelemetry
How to trace requests with Axios and OpenTelemetry
Right now I’m consulting folks, who are building new EdTech platform.
As a big fan of API specifications - OpenAPI, AsyncAPI, I decided to start developing from contracts
API client initialization
For OpenAPI I choose openapi-client-axios. Few lines of code and I have pre-built Axios client for interaction with our platform API
Let’s define out client
1
2
3
4
const chatAPI = new OpenAPIClientAxios({
definition: '/chat-api.json',
axiosConfigDefaults: {},
});
Now it’s time to initialize it
1
2
3
4
5
6
onMounted(async () => {
chatAPIClient.value = await chatAPI.init();
chatService.value = new ChatService(chatAPI, chatAPIClient.value);
chats.value = await chatService.value.requestChats();
})
And, it’s done. Now we have typed client, generated from OpenAPI specification
OpenTelemetry initialization
When API client is ready, it’s type to wrap it up with OpenTelemetry, to start sending traces to our APM.
I’ll use Open-Source version of SigNoz APM
Let’s define our OpenTelemetry
Installing OpenTelemetry dependencies
1
2
3
4
5
6
7
nmp i @opentelemetry/sdk-trace-web \
@opentelemetry/context-zone \
@opentelemetry/resources \
@opentelemetry/semantic-conventions \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/core \
@opentelemetry/api
Configuring resource
After installing of OpenTelemetry dependencies, it’s time to configure our resource
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {Resource} from '@opentelemetry/resources';
import {
SEMRESATTRS_DEPLOYMENT_ENVIRONMENT,
SEMRESATTRS_SERVICE_NAME,
SEMRESATTRS_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';
const resource = Resource.default().merge(
new Resource({
[SEMRESATTRS_SERVICE_NAME]: '{application_name}', // for example: chat
[SEMRESATTRS_SERVICE_VERSION]: '{application_version}', // for example: 23.78.0-RC2
[SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: '{application_environment}', // for example: prod, dev
})
);
Configuring provider
When resource is ready, it’s time to configure provider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {BatchSpanProcessor, WebTracerProvider} from '@opentelemetry/sdk-trace-web';
import {OTLPTraceExporter} from "@opentelemetry/exporter-trace-otlp-http";
import {ZoneContextManager} from '@opentelemetry/context-zone';
import {W3CTraceContextPropagator} from "@opentelemetry/core";
const provider = new WebTracerProvider({ resource });
// Uncomment to see traces in web console
// provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
provider.addSpanProcessor(
new BatchSpanProcessor(
new OTLPTraceExporter({
url: 'http://{signoz_address}:4318/v1/traces',
})
)
);
provider.register({
// Changing default contextManager to use ZoneContextManager - supports asynchronous operations - optional
contextManager: new ZoneContextManager(),
propagator: new W3CTraceContextPropagator(), // Send in W3C format
});
Configuring tracing
Because Axios doesn’t use Fetch API, we can’t automatically send traces via extensions.
That’s why we need to introduce proxy function, which will send traces to our APM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import {Operation} from "openapi-client-axios";
import {AxiosResponse} from "axios";
export const webTracerWithZone = provider.getTracer('{application_name}');
export async function traceSpan<F extends (...args: any) => Promise<AxiosResponse<any, any>>>(
operation: Operation,
operationBaseUrl: string,
apiCall: F
): Promise<AxiosResponse<any, any>> {
return webTracerWithZone.startActiveSpan(operation.operationId!, async (span: Span) => {
try {
console.info(`requesting ${operation.operationId}`);
const apiResponse: AxiosResponse<any, any> = await apiCall();
span.setAttribute('http.method', operation.method);
span.setAttribute('http.url', `${operationBaseUrl}${operation.path}`);
span.setAttribute('http.status_code', apiResponse.status);
return apiResponse;
} catch (error) {
span.setStatus({code: SpanStatusCode.ERROR, message: `${error}`});
throw error;
} finally {
span.end();
}
})
}
Resulted OpenTelemetry configuration
Here is full OpenTelemetry configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import {BatchSpanProcessor, WebTracerProvider} from '@opentelemetry/sdk-trace-web';
import {ZoneContextManager} from '@opentelemetry/context-zone';
import {Resource} from '@opentelemetry/resources';
import {
SEMRESATTRS_DEPLOYMENT_ENVIRONMENT,
SEMRESATTRS_SERVICE_NAME,
SEMRESATTRS_SERVICE_VERSION,
} from '@opentelemetry/semantic-conventions';
import {OTLPTraceExporter} from "@opentelemetry/exporter-trace-otlp-http";
import {W3CTraceContextPropagator} from "@opentelemetry/core";
import {Span, SpanStatusCode} from "@opentelemetry/api";
import {Operation} from "openapi-client-axios";
import {AxiosResponse} from "axios";
const resource = Resource.default().merge(
new Resource({
[SEMRESATTRS_SERVICE_NAME]: '{application_name}', // for example: chat
[SEMRESATTRS_SERVICE_VERSION]: '{application_version}', // for example: 23.78.0-RC2
[SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: '{application_environment}', // for example: prod, dev
})
);
const provider = new WebTracerProvider({ resource });
// provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
provider.addSpanProcessor(
new BatchSpanProcessor(
new OTLPTraceExporter({
url: 'http://{signoz_address}:4318/v1/traces',
})
)
);
provider.register({
// Changing default contextManager to use ZoneContextManager - supports asynchronous operations - optional
contextManager: new ZoneContextManager(),
propagator: new W3CTraceContextPropagator(),
});
export const webTracerWithZone = provider.getTracer('{application_name}');
export async function traceSpan<F extends (...args: any) => Promise<AxiosResponse<any, any>>>(
operation: Operation,
operationBaseUrl: string,
apiCall: F
): Promise<AxiosResponse<any, any>> {
return webTracerWithZone.startActiveSpan(operation.operationId!, async (span: Span) => {
try {
console.info(`requesting ${operation.operationId}`);
const apiResponse: AxiosResponse<any, any> = await apiCall();
span.setAttribute('http.method', operation.method);
span.setAttribute('http.url', `${operationBaseUrl}${operation.path}`);
span.setAttribute('http.status_code', apiResponse.status);
return apiResponse;
} catch (error) {
span.setStatus({code: SpanStatusCode.ERROR, message: `${error}`});
throw error;
} finally {
span.end();
}
})
}
Tracing API client requests
Let’s assume that we have ChatService
to wrap common logic
To trace requests we need to add next dependencies
1
2
3
4
5
6
import {traceSpan} from "@/opentelemetry.ts";
import {Client, Components} from "@api/chatapi";
import {AxiosResponse} from "axios";
import {OpenAPIClientAxios, Operation} from "openapi-client-axios";
import {ExplicitParamValue, HttpMethod} from "openapi-client-axios/types/client";
and add proxy function, like this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private async apiCall(
operationId: string,
parameters: Array<ExplicitParamValue> | undefined = undefined,
data: {} | undefined = undefined
): Promise<AxiosResponse<any, any>> {
const operation: Operation = this.chatApi.getOperation(operationId)!
const operationBaseUrl: string = this.chatApi.getBaseURL(operation)!;
return await traceSpan(
operation,
operationBaseUrl,
async (): Promise<AxiosResponse<any, any>> => {
if (operation.method === HttpMethod.Get) {
// @ts-ignore No index signature with a parameter of type string was found on type PathsDictionary
return await this.chatAPIClient.paths[`${operation.path}`].get(parameters);
} else {
// @ts-ignore No index signature with a parameter of type string was found on type PathsDictionary
return await this.chatAPIClient.paths[`${operation.path}`].post(parameters, data);
}
});
}
now we can make request like this
1
2
3
4
5
6
7
8
9
public async requestChats(): Promise<Chat[]> {
try {
const apiResponse = await this.apiCall('getChats'); // operationId from OpenAPI
return this.handleRequestChats(apiResponse);
} catch (error) {
console.error(error);
return [];
}
}
here is result