feat: harmonize log and metrics

This commit is contained in:
Michael Zetterberg
2025-04-17 07:16:11 +02:00
parent 858a81b16f
commit 5323a8e46e
58 changed files with 2324 additions and 4726 deletions

View File

@@ -5,6 +5,8 @@ import * as api from "@/lib/api"
import { dt } from "@/lib/dt"
import { badRequestError, unauthorizedError } from "@/server/errors/trpc"
import { getCityPageUrls } from "@/server/routers/contentstack/destinationCityPage/utils"
import { getVerifiedUser } from "@/server/routers/user/utils"
import { createCounter } from "@/server/telemetry"
import {
contentStackBaseWithServiceProcedure,
publicProcedure,
@@ -17,7 +19,6 @@ import { toApiLang } from "@/server/utils"
import { getCacheClient } from "@/services/dataCache"
import { getHotelPageUrls } from "../contentstack/hotelPage/utils"
import { getVerifiedUser } from "../user/query"
import { additionalDataSchema } from "./schemas/hotel/include/additionalData"
import { meetingRoomsSchema } from "./schemas/meetingRoom"
import {
@@ -42,17 +43,11 @@ import {
selectRateRoomAvailabilityInputSchema,
selectRateRoomsAvailabilityInputSchema,
} from "./input"
import { metrics } from "./metrics"
import {
ancillaryPackagesSchema,
breakfastPackagesSchema,
getNearbyHotelIdsSchema,
} from "./output"
import {
locationsUrlsCounter,
locationsUrlsFailCounter,
locationsUrlsSuccessCounter,
} from "./telemetry"
import {
getBedTypes,
getCitiesByCountry,
@@ -431,36 +426,24 @@ export const hotelQueryRouter = router({
const { lang } = ctx
const apiLang = toApiLang(lang)
metrics.hotelsAvailabilityBookingCode.counter.add(1, {
...input,
})
const bookingCodeAvailabilityResponse =
await getHotelsAvailabilityByCity(input, apiLang, ctx.serviceToken)
// If API or network failed with no response
if (!bookingCodeAvailabilityResponse) {
metrics.hotelsAvailabilityBookingCode.fail.add(1, {
...input,
error_type: "unknown",
})
return null
}
// Get regular availability of hotels which don't have availability with booking code.
const unavailableHotelIds =
bookingCodeAvailabilityResponse?.availability
.filter((hotel) => {
return hotel.status === "NotAvailable"
})
.flatMap((hotel) => {
return hotel.hotelId
})
const unavailableHotelIds = bookingCodeAvailabilityResponse.availability
.filter((hotel) => {
return hotel.status === "NotAvailable"
})
.flatMap((hotel) => {
return hotel.hotelId
})
// All hotels have availability with booking code no need to fetch regular prices.
// return response as is without any filtering as below.
if (!unavailableHotelIds || !unavailableHotelIds.length) {
return bookingCodeAvailabilityResponse
}
const unavailableHotelsInput = {
...input,
bookingCode: "",
@@ -472,11 +455,6 @@ export const hotelQueryRouter = router({
ctx.serviceToken
)
metrics.hotelsAvailabilityBookingCode.success.add(1, {
...input,
})
console.info("api.hotels.hotelsAvailabilityBookingCode success")
// No regular rates available due to network or API failure (no need to filter & merge).
if (!unavailableHotels) {
return bookingCodeAvailabilityResponse
@@ -537,19 +515,16 @@ export const hotelQueryRouter = router({
const language = ctx.lang
let hotelsToFetch: string[] = []
metrics.hotels.counter.add(1, {
input: JSON.stringify(input),
const getHotelsByCSFilterCounter = createCounter(
"trpc.hotel.hotels",
"byCSFilter"
)
const metricsGetHotelsByCSFilter = getHotelsByCSFilterCounter.init({
input,
language,
})
console.info(
"api.hotel.hotels start",
JSON.stringify({
query: {
...input,
language,
},
})
)
metricsGetHotelsByCSFilter.start()
if (hotelsToInclude.length) {
hotelsToFetch = hotelsToInclude
@@ -571,20 +546,13 @@ export const hotelQueryRouter = router({
.find((loc) => loc.cityIdentifier === locationFilter.city)?.id
if (!cityId) {
metrics.hotels.fail.add(1, {
input: JSON.stringify(input),
language,
error_type: "not_found",
error: `CityId not found for cityIdentifier: ${locationFilter.city}`,
})
console.error(
"api.hotel.hotels not found error",
JSON.stringify({
query: { ...input, language },
error: `CityId not found for cityIdentifier: ${locationFilter.city}`,
})
metricsGetHotelsByCSFilter.dataError(
`CityId not found for cityIdentifier: ${locationFilter.city}`,
{
cityIdentifier: locationFilter.city,
}
)
return []
}
@@ -594,20 +562,13 @@ export const hotelQueryRouter = router({
})
if (!hotelIds?.length) {
metrics.hotels.fail.add(1, {
cityId,
language,
error_type: "not_found",
error: `No hotelIds found for cityId: ${cityId}`,
})
console.error(
"api.hotel.hotels not found error",
JSON.stringify({
query: { cityId, language },
error: `No hotelIds found for cityId: ${cityId}`,
})
metricsGetHotelsByCSFilter.dataError(
`No hotelIds found for cityId: ${cityId}`,
{
cityId,
}
)
return []
}
@@ -623,20 +584,13 @@ export const hotelQueryRouter = router({
})
if (!hotelIds?.length) {
metrics.hotels.fail.add(1, {
country: locationFilter.country,
language,
error_type: "not_found",
error: `No hotelIds found for country: ${locationFilter.country}`,
})
console.error(
"api.hotel.hotels not found error",
JSON.stringify({
query: { country: locationFilter.country, language },
error: `No hotelIds found for cityId: ${locationFilter.country}`,
})
metricsGetHotelsByCSFilter.dataError(
`No hotelIds found for country: ${locationFilter.country}`,
{
country: locationFilter.country,
}
)
return []
}
@@ -648,20 +602,11 @@ export const hotelQueryRouter = router({
}
if (!hotelsToFetch.length) {
metrics.hotels.fail.add(1, {
input: JSON.stringify(input),
language,
error_type: "not_found",
error: `Couldn't find any hotels for given input: ${JSON.stringify(input)}`,
})
console.error(
"api.hotel.hotels not found error",
JSON.stringify({
query: JSON.stringify(input),
error: `Couldn't find any hotels for given input: ${JSON.stringify(input)}`,
})
metricsGetHotelsByCSFilter.dataError(
`Couldn't find any hotels for given input: ${JSON.stringify(input)}`,
input
)
return []
}
const hotelPages = await getHotelPageUrls(language)
@@ -684,20 +629,7 @@ export const hotelQueryRouter = router({
})
)
metrics.hotels.success.add(1, {
input: JSON.stringify(input),
language,
})
console.info(
"api.hotels success",
JSON.stringify({
query: {
input: JSON.stringify(input),
language,
},
})
)
metricsGetHotelsByCSFilter.success()
return hotels.filter((hotel): hotel is HotelDataWithUrl => !!hotel)
}),
@@ -769,13 +701,17 @@ export const hotelQueryRouter = router({
return cacheClient.cacheOrGet(
`${apiLang}:nearbyHotels:${hotelId}`,
async () => {
metrics.nearbyHotelIds.counter.add(1, {
const nearbyHotelsCounter = createCounter(
"trpc.hotel",
"nearbyHotelIds"
)
const metricsNearbyHotels = nearbyHotelsCounter.init({
params,
hotelId,
})
console.info(
"api.hotels.nearbyHotelIds start",
JSON.stringify({ query: { hotelId, params } })
)
metricsNearbyHotels.start()
const apiResponse = await api.get(
api.endpoints.v1.Hotel.Hotels.nearbyHotels(hotelId),
{
@@ -786,55 +722,18 @@ export const hotelQueryRouter = router({
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
metrics.nearbyHotelIds.fail.add(1, {
hotelId,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.nearbyHotelIds error",
JSON.stringify({
query: { hotelId, params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
await metricsNearbyHotels.httpError(apiResponse)
return null
}
const apiJson = await apiResponse.json()
const validateHotelData = getNearbyHotelIdsSchema.safeParse(apiJson)
if (!validateHotelData.success) {
metrics.nearbyHotelIds.fail.add(1, {
hotelId,
error_type: "validation_error",
error: JSON.stringify(validateHotelData.error),
})
console.error(
"api.hotels.nearbyHotelIds validation error",
JSON.stringify({
query: { hotelId, params },
error: validateHotelData.error,
})
)
metricsNearbyHotels.validationError(validateHotelData.error)
throw badRequestError()
}
metrics.nearbyHotelIds.success.add(1, {
hotelId,
})
console.info(
"api.hotels.nearbyHotelIds success",
JSON.stringify({
query: { hotelId, params },
})
)
metricsNearbyHotels.success()
return validateHotelData.data.map((id: string) => parseInt(id, 10))
},
@@ -884,16 +783,17 @@ export const hotelQueryRouter = router({
urls: publicProcedure
.input(getLocationsUrlsInput)
.query(async ({ input }) => {
const procedureName = "hotels.locations.urls"
const { lang } = input
locationsUrlsCounter.add(1, { lang })
console.info(
`${procedureName}: start`,
JSON.stringify({ query: { lang } })
const locationsUrlsCounter = createCounter(
"trpc.hotel.locations",
"urls"
)
const metricsLocationsUrls = locationsUrlsCounter.init({
lang,
})
metricsLocationsUrls.start()
const [hotelPageUrlsResult, cityPageUrlsResult] =
await Promise.allSettled([
@@ -905,32 +805,15 @@ export const hotelQueryRouter = router({
hotelPageUrlsResult.status === "rejected" ||
cityPageUrlsResult.status === "rejected"
) {
locationsUrlsFailCounter.add(1, {
lang,
error_type: "no_data",
response: JSON.stringify({
hotelPageUrlsResult,
cityPageUrlsResult,
}),
})
console.error(`${procedureName}: no data`, {
variables: { lang },
error_type: "no_data",
response: {
hotelPageUrlsResult,
cityPageUrlsResult,
},
metricsLocationsUrls.dataError(`Failed to get data for page URLs`, {
hotelPageUrlsResult,
cityPageUrlsResult,
})
return null
}
locationsUrlsSuccessCounter.add(1, { lang })
console.info(`${procedureName}: success`, {
variables: { lang },
})
metricsLocationsUrls.success()
return {
hotels: hotelPageUrlsResult.value,
@@ -992,12 +875,12 @@ export const hotelQueryRouter = router({
hotelId,
language,
}
const metricsData = { ...params, hotelId: input.hotelId }
metrics.meetingRooms.counter.add(1, metricsData)
console.info(
"api.hotels.meetingRooms start",
JSON.stringify({ query: { hotelId, params } })
)
const meetingRoomsCounter = createCounter("trpc.hotel", "meetingRooms")
const metricsMeetingRooms = meetingRoomsCounter.init({
params,
})
metricsMeetingRooms.start()
const cacheClient = await getCacheClient()
return cacheClient.cacheOrGet(
@@ -1014,28 +897,7 @@ export const hotelQueryRouter = router({
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
metrics.meetingRooms.fail.add(1, {
...metricsData,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.meetingRooms error",
JSON.stringify({
query: { params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
await metricsMeetingRooms.httpError(apiResponse)
throw new Error("Failed to fetch meeting rooms")
}
@@ -1043,22 +905,10 @@ export const hotelQueryRouter = router({
const validatedMeetingRooms = meetingRoomsSchema.safeParse(apiJson)
if (!validatedMeetingRooms.success) {
console.error(
"api.hotels.meetingRooms validation error",
JSON.stringify({
query: { params },
error: validatedMeetingRooms.error,
})
)
metricsMeetingRooms.validationError(validatedMeetingRooms.error)
throw badRequestError()
}
metrics.meetingRooms.success.add(1, {
hotelId,
})
console.info(
"api.hotels.meetingRooms success",
JSON.stringify({ query: { params } })
)
metricsMeetingRooms.success()
return validatedMeetingRooms.data.data
},
@@ -1074,12 +924,16 @@ export const hotelQueryRouter = router({
hotelId,
language,
}
const metricsData = { ...params, hotelId: input.hotelId }
metrics.additionalData.counter.add(1, metricsData)
console.info(
"api.hotels.additionalData start",
JSON.stringify({ query: { hotelId, params } })
const additionalDataCounter = createCounter(
"trpc.hotel",
"additionalData"
)
const metricsAdditionalData = additionalDataCounter.init({
params,
})
metricsAdditionalData.start()
const cacheClient = await getCacheClient()
return cacheClient.cacheOrGet(
@@ -1096,28 +950,7 @@ export const hotelQueryRouter = router({
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
metrics.additionalData.fail.add(1, {
...metricsData,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.additionalData error",
JSON.stringify({
query: { params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
await metricsAdditionalData.httpError(apiResponse)
throw new Error("Unable to fetch additional data for hotel")
}
@@ -1126,22 +959,11 @@ export const hotelQueryRouter = router({
additionalDataSchema.safeParse(apiJson)
if (!validatedAdditionalData.success) {
console.error(
"api.hotels.additionalData validation error",
JSON.stringify({
query: { params },
error: validatedAdditionalData.error,
})
)
metricsAdditionalData.validationError(validatedAdditionalData.error)
throw badRequestError()
}
metrics.additionalData.success.add(1, {
hotelId,
})
console.info(
"api.hotels.additionalData success",
JSON.stringify({ query: { params } })
)
metricsAdditionalData.success()
return validatedAdditionalData.data
},
@@ -1167,12 +989,16 @@ export const hotelQueryRouter = router({
language: apiLang,
}
const metricsData = { ...params, hotelId: input.hotelId }
metrics.breakfastPackage.counter.add(1, metricsData)
console.info(
"api.package.breakfast start",
JSON.stringify({ query: metricsData })
const breakfastCounter = createCounter(
"trpc.hotel.packages",
"breakfast"
)
const metricsBreakfast = breakfastCounter.init({
params,
hotelId: input.hotelId,
})
metricsBreakfast.start()
const cacheClient = await getCacheClient()
const breakfastPackages = await cacheClient.cacheOrGet(
@@ -1189,57 +1015,17 @@ export const hotelQueryRouter = router({
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
metrics.breakfastPackage.fail.add(1, {
...metricsData,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.package.breakfast error",
JSON.stringify({
query: metricsData,
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
await metricsBreakfast.httpError(apiResponse)
throw new Error("Unable to fetch breakfast packages")
}
const apiJson = await apiResponse.json()
const breakfastPackages = breakfastPackagesSchema.safeParse(apiJson)
if (!breakfastPackages.success) {
metrics.breakfastPackage.fail.add(1, {
...metricsData,
error_type: "validation_error",
error: JSON.stringify(breakfastPackages.error),
})
console.error(
"api.package.breakfast validation error",
JSON.stringify({
query: metricsData,
error: breakfastPackages.error,
})
)
metricsBreakfast.validationError(breakfastPackages.error)
throw new Error("Unable to parse breakfast packages")
}
metrics.breakfastPackage.success.add(1, metricsData)
console.info(
"api.package.breakfast success",
JSON.stringify({
query: metricsData,
})
)
return breakfastPackages.data
},
"1h"
@@ -1265,6 +1051,8 @@ export const hotelQueryRouter = router({
// }
// }
metricsBreakfast.success()
return breakfastPackages.filter(
(pkg) => pkg.code !== BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
)
@@ -1281,16 +1069,21 @@ export const hotelQueryRouter = router({
language: apiLang,
}
const ancillaryCounter = createCounter(
"trpc.hotel.packages",
"ancillary"
)
const metricsAncillary = ancillaryCounter.init({
params,
hotelId: input.hotelId,
})
metricsAncillary.start()
const cacheClient = await getCacheClient()
return await cacheClient.cacheOrGet(
const result = await cacheClient.cacheOrGet(
`${apiLang}:hotel:${input.hotelId}:ancillaries:startDate:${params.StartDate}:endDate:${params.EndDate}`,
async () => {
const metricsData = { ...params, hotelId: input.hotelId }
metrics.ancillaryPackage.counter.add(1, metricsData)
console.info(
"api.package.ancillary start",
JSON.stringify({ query: metricsData })
)
const apiResponse = await api.get(
api.endpoints.v1.Package.Ancillary.hotel(input.hotelId),
{
@@ -1302,59 +1095,25 @@ export const hotelQueryRouter = router({
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
metrics.ancillaryPackage.fail.add(1, {
...metricsData,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.package.ancillary start error",
JSON.stringify({
query: metricsData,
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
await metricsAncillary.httpError(apiResponse)
return null
}
const apiJson = await apiResponse.json()
const ancillaryPackages = ancillaryPackagesSchema.safeParse(apiJson)
if (!ancillaryPackages.success) {
metrics.ancillaryPackage.fail.add(1, {
...metricsData,
error_type: "validation_error",
error: JSON.stringify(ancillaryPackages.error),
})
console.error(
"api.package.ancillary validation error",
JSON.stringify({
query: metricsData,
error: ancillaryPackages.error,
})
)
metricsAncillary.validationError(ancillaryPackages.error)
return null
}
metrics.ancillaryPackage.success.add(1, metricsData)
console.info(
"api.package.ancillary success",
JSON.stringify({
query: metricsData,
})
)
return ancillaryPackages.data
},
"1h"
)
metricsAncillary.success()
return result
}),
}),
})