import { AxiosFulfilledInterceptor, AxiosInterceptor, AxiosRejectedInterceptor, AxiosResponseCustomConfig, } from "@narando/nest-axios-interceptor"; import { Injectable } from "@nestjs/common"; import { HttpService } from "@nestjs/axios"; import { Subsegment, utils } from "aws-xray-sdk"; import { AxiosRequestConfig } from "axios"; import { ClientRequest, IncomingMessage } from "http"; import { TracingService } from "../../core"; import { TracingNotInitializedException } from "../../exceptions"; import { HEADER_TRACE_CONTEXT } from "./http-tracing.constants"; export const TRACING_CONFIG_KEY = Symbol("kTracingAxiosInterceptor"); export interface TracingConfig extends AxiosRequestConfig { [TRACING_CONFIG_KEY]?: { subSegment: Subsegment; }; } @Injectable() export class TracingAxiosInterceptor extends AxiosInterceptor<TracingConfig> { constructor( private readonly tracingService: TracingService, httpService: HttpService ) { super(httpService); } public requestFulfilled(): AxiosFulfilledInterceptor<TracingConfig> { // Add Info to Subsegment return (config) => { // Create Subsegment try { const subSegment = this.tracingService.createSubSegment("http-call"); config[TRACING_CONFIG_KEY] = { subSegment, }; if (!config.headers) { config.headers = {}; } config.headers[HEADER_TRACE_CONTEXT] = this.tracingService.getTracingHeader(subSegment); return config; } catch (err) { if (err instanceof TracingNotInitializedException) { // TODO: Define proper behaviour for this case: // - should we create a new root segment for this? // - should we log the error so the user can investigate why its occuring? return config; } throw err; } }; } public requestRejected(): AxiosRejectedInterceptor { // Cause: Networking Error // Add error to Subsegment // Close Subsegment return (error) => { if (this.isAxiosError(error)) { try { const subSegment = this.getSubSegmentFromConfig(error.config); if (subSegment) { subSegment.addError(error); subSegment.addFaultFlag(); subSegment.close(error); } } catch (tracingError) { // request error is "more important" than the error from tracing // so we swallow the tracing exception (probably TracingNotInitializedException) } } throw error; }; } public responseFulfilled(): AxiosFulfilledInterceptor< AxiosResponseCustomConfig<TracingConfig> > { // Add response code to Subsegment // Close Subsegment return (response) => { try { const subSegment = this.getSubSegmentFromConfig(response.config); if (subSegment) { subSegment.addRemoteRequestData( response.request, { statusCode: response.status, headers: response.headers, } as IncomingMessage, true ); const cause = utils.getCauseTypeFromHttpStatus(response.status); switch (cause) { case "error": subSegment.addErrorFlag(); break; case "fault": subSegment.addFaultFlag(); break; case undefined: default: break; } subSegment.close(); } } catch (err) { if (err instanceof TracingNotInitializedException) { return response; } throw err; } return response; }; } public responseRejected(): AxiosRejectedInterceptor { // Non 2xx Status Code // Add error to Subsegment // Close Subsegment return (error) => { if (this.isAxiosError(error)) { try { const subSegment = this.getSubSegmentFromConfig(error.config); if (subSegment) { if (error.request && error.response) { const request = error.request as ClientRequest; const response = { statusCode: error.response.status, } as IncomingMessage; subSegment.addRemoteRequestData(request, response, true); } else if (error.config) { // Networking Error // TODO: Implement addRemoteRequestData } subSegment.close(error); } } catch (tracingError) { // response error is "more important" than the error from tracing // so we swallow the tracing exception (probably TracingNotInitializedException) } } throw error; }; } private getSubSegmentFromConfig(config: TracingConfig): Subsegment | null { const tracingData = config[TRACING_CONFIG_KEY]; if (tracingData !== undefined && tracingData.subSegment) { return tracingData.subSegment; } return null; } }