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
159 changes: 77 additions & 82 deletions packages/serviceSearchClient/src/live-serviceSearch-client.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import {Logger} from "@aws-lambda-powertools/logger"
import {getSecret} from "@aws-lambda-powertools/parameters/secrets"
import axios, {AxiosError, AxiosInstance} from "axios"
import axios, {
AxiosError,
AxiosHeaders,
AxiosInstance,
isAxiosError
} from "axios"
import axiosRetry, {isNetworkOrIdempotentRequestError} from "axios-retry"
import {handleUrl} from "./handleUrl"
import {Agent} from "https"

import {ServiceSearchClient} from "./serviceSearch-client"

// timeout in ms to wait for response from serviceSearch to avoid lambda timeout
// each call is retried up to 3 times so total wait time could be up to 4x this value
const SERVICE_SEARCH_TIMEOUT = 1000 // 1 second
const SERVICE_SEARCH_TIMEOUT = 3000 // 3 seconds
const DISTANCE_SELLING = "DistanceSelling"

type Service = {
Expand Down Expand Up @@ -67,6 +73,7 @@ export function getServiceSearchEndpoint(logger: Logger | null = null): string {
export class LiveServiceSearchClient implements ServiceSearchClient {
private readonly axiosInstance: AxiosInstance
private readonly logger: Logger
private readonly httpsAgent: Agent
private readonly outboundHeaders: {
"apikey"?: string,
"Subscription-Key"?: string,
Expand All @@ -81,9 +88,15 @@ export class LiveServiceSearchClient implements ServiceSearchClient {
v2: process.env.ServiceSearchApiKey !== undefined,
v3: process.env.ServiceSearch3ApiKey !== undefined
})
this.axiosInstance = axios.create()
this.httpsAgent = new Agent({
keepAlive: true
})
this.axiosInstance = axios.create({
httpsAgent: this.httpsAgent
})
axiosRetry(this.axiosInstance, {
retries: 3,
shouldResetTimeout: true,
onRetry: this.onAxiosRetry,
retryCondition: this.retryCondition
})
Expand All @@ -95,38 +108,40 @@ export class LiveServiceSearchClient implements ServiceSearchClient {
this.axiosInstance.interceptors.response.use((response) => {
const currentTime = Date.now()
const startTime = response.config.headers["request-startTime"]
this.logger.info("serviceSearch request duration", {serviceSearch_duration: currentTime - startTime})
this.logger.info("serviceSearch request duration", {
serviceSearch_duration: currentTime - startTime,
serviceSearch_keepAliveEnabled: this.httpsAgent.options.keepAlive === true,
serviceSearch_reusedSocket: this.getReusedSocket(response.request)
})

return response
}, (error) => {
const currentTime = Date.now()
const startTime = error.config?.headers["request-startTime"]
this.logger.info("serviceSearch request duration", {serviceSearch_duration: currentTime - startTime})
this.logger.info("serviceSearch request duration", {
serviceSearch_duration: currentTime - startTime,
serviceSearch_keepAliveEnabled: this.httpsAgent.options.keepAlive === true,
serviceSearch_reusedSocket: this.getReusedSocket(error.request)
})

// reject with a proper Error object
let err: Error
if (error instanceof Error) {
if (isAxiosError(error)) {
this.stripApiKeyFromHeaders(error)
this.logger.error("Axios error in serviceSearch request", {
axiosErrorDetails: {
request: error.request,
response: {
data: error.response?.data,
status: error.response?.status,
headers: error.response?.headers as AxiosHeaders
}
}
})
err = new Error(`Axios error in serviceSearch request: ${error.message}`)
} else if (error instanceof Error) {
this.logger.error("Error in serviceSearch request", {error})
err = error
} else if ((error as AxiosError).message) {
// Only report the interesting subset of the error object.
let axiosErrorDetails = {}
if (error.response) {
axiosErrorDetails = {response: {
data: error.data,
status: error.status,
headers: error.headers
}}
}
if (error.request) {
axiosErrorDetails = {
...axiosErrorDetails,
request: error.request
}
}

this.logger.error("Axios error in serviceSearch request", {axiosErrorDetails})
err = new Error("Axios error in serviceSearch request")
} else {
this.logger.error("Unknown error in serviceSearch request", {error})
err = new Error("Unknown error in serviceSearch request")
Expand Down Expand Up @@ -168,69 +183,49 @@ export class LiveServiceSearchClient implements ServiceSearchClient {
}

async searchService(odsCode: string, correlationId: string): Promise<URL | undefined> {
try {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks more than it is: actually just removes the double error logging in favour of interceptor logging only

// Load API key if not set in environment (secrets layer is failing to load v3 key)
const apiVsn = getServiceSearchVersion(this.logger)
if (apiVsn === 3 && !this.outboundHeaders.apikey) {
this.logger.info("API key not in environment, attempting to load from Secrets Manager")
this.outboundHeaders.apikey = await this.loadApiKeyFromSecretsManager()
}
this.outboundHeaders["x-correlation-id"] = correlationId
const xRequestId = crypto.randomUUID()
this.outboundHeaders["x-request-id"] = xRequestId

const address = getServiceSearchEndpoint(this.logger)
const queryParams = {...SERVICE_SEARCH_BASE_QUERY_PARAMS, search: odsCode}
// Load API key if not set in environment (secrets layer is failing to load v3 key)
const apiVsn = getServiceSearchVersion(this.logger)
if (apiVsn === 3 && !this.outboundHeaders.apikey) {
this.logger.info("API key not in environment, attempting to load from Secrets Manager")
this.outboundHeaders.apikey = await this.loadApiKeyFromSecretsManager()
}
this.outboundHeaders["x-correlation-id"] = correlationId
const xRequestId = crypto.randomUUID()
this.outboundHeaders["x-request-id"] = xRequestId

this.logger.info(`making request to ${address} with ods code ${odsCode}`, {
odsCode: odsCode,
requestHeaders: {
"x-request-id": xRequestId,
"x-correlation-id": correlationId
}
})
const response = await this.axiosInstance.get(address, {
headers: this.outboundHeaders,
params: queryParams,
timeout: SERVICE_SEARCH_TIMEOUT
})
const address = getServiceSearchEndpoint(this.logger)
const queryParams = {...SERVICE_SEARCH_BASE_QUERY_PARAMS, search: odsCode}

this.logger.info(`received response from serviceSearch for ods code ${odsCode}`,
{odsCode: odsCode, status: response.status, data: response.data})
if (apiVsn === 2) {
return this.handleV2Response(odsCode, response.data)
} else {
return this.handleV3Response(odsCode, response.data)
this.logger.info(`making request to ${address} with ods code ${odsCode}`, {
odsCode: odsCode,
requestHeaders: {
"x-request-id": xRequestId,
"x-correlation-id": correlationId
}
})
const response = await this.axiosInstance.get(address, {
headers: this.outboundHeaders,
params: queryParams,
timeout: SERVICE_SEARCH_TIMEOUT
})

} catch (error) {
if (axios.isAxiosError(error)) {
this.stripApiKeyFromHeaders(error)
if (error.response) {
this.logger.error("error in response from serviceSearch", {
response: {
data: error.response.data,
status: error.response.status,
Headers: error.response.headers
},
request: {
method: error.request?.path,
params: error.request?.params,
headers: error.request?.headers,
host: error.request?.host
}
})
} else if (error.request) {
this.logger.error("error in request to serviceSearch", {error})
} else {
this.logger.error("general error calling serviceSearch", {error})
}
} else {
this.logger.error("general error", {error})
}
throw error
this.logger.info(`received response from serviceSearch for ods code ${odsCode}`,
{odsCode: odsCode, status: response.status, data: response.data})
if (apiVsn === 2) {
return this.handleV2Response(odsCode, response.data)
} else {
return this.handleV3Response(odsCode, response.data)
}
}

private getReusedSocket(request: unknown): boolean | undefined {
if (!request || typeof request !== "object") {
return undefined
}
const candidate = request as {reusedSocket?: boolean}
return candidate.reusedSocket
}

handleV3Response(odsCode: string, data: ServiceSearch3Data): URL | undefined {
const contacts = data.value[0]?.Contacts
const websiteContact = contacts?.find((contact: Contact) => contact.ContactMethodType === "Website")
Expand Down
Loading