From 1ff5d57dedc455ce4b6057938f45cf1efbabeeba Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:21:54 +0530 Subject: [PATCH] feat: distributed tracing - 2 (#24861) * feat: distributed tracing 2 * feat: distributed tracing 2 * refactor: feedback * refactor: feedback * fix: type error * fix: trpc error --------- Co-authored-by: Hariom Balhara --- apps/web/pages/api/book/event.ts | 7 +- packages/features/bookings/lib/dto/types.d.ts | 2 + .../lib/service/RegularBookingService.ts | 123 +++++++++++------- packages/lib/server/defaultResponder.test.ts | 24 +++- packages/lib/server/defaultResponder.ts | 32 ++++- packages/lib/tracing/index.ts | 23 +++- packages/trpc/server/createContext.ts | 14 +- 7 files changed, 158 insertions(+), 67 deletions(-) diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index 7f5aa63d87..3782520a94 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -10,10 +10,11 @@ import getIP from "@calcom/lib/getIP"; import { piiHasher } from "@calcom/lib/server/PiiHasher"; import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken"; import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import prisma from "@calcom/prisma"; +import type { TraceContext } from "@calcom/lib/tracing"; +import { prisma } from "@calcom/prisma"; import { CreationSource } from "@calcom/prisma/enums"; -async function handler(req: NextApiRequest & { userId?: number }) { +async function handler(req: NextApiRequest & { userId?: number; traceContext: TraceContext }) { const userIp = getIP(req); if (process.env.NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1") { @@ -52,8 +53,10 @@ async function handler(req: NextApiRequest & { userId?: number }) { userId: session?.user?.id || -1, hostname: req.headers.host || "", forcedSlug: req.headers["x-cal-force-slug"] as string | undefined, + traceContext: req.traceContext, }, }); + // const booking = await createBookingThroughFactory(); return booking; diff --git a/packages/features/bookings/lib/dto/types.d.ts b/packages/features/bookings/lib/dto/types.d.ts index 386b71e993..75e6efc10b 100644 --- a/packages/features/bookings/lib/dto/types.d.ts +++ b/packages/features/bookings/lib/dto/types.d.ts @@ -4,6 +4,7 @@ */ import type getBookingDataSchema from "@calcom/features/bookings/lib/getBookingDataSchema"; import type getBookingDataSchemaForApi from "@calcom/features/bookings/lib/getBookingDataSchemaForApi"; +import type { TraceContext } from "@calcom/lib/tracing"; import type { SchedulingType } from "@calcom/prisma/enums"; import type { ExtendedBookingCreateBody } from "../bookingCreateBodySchema"; @@ -37,6 +38,7 @@ export type CreateBookingMeta = { hostname?: string; forcedSlug?: string; noEmail?: boolean; + traceContext?: TraceContext; } & PlatformParams; export type BookingHandlerInput = { diff --git a/packages/features/bookings/lib/service/RegularBookingService.ts b/packages/features/bookings/lib/service/RegularBookingService.ts index 3b427d350c..f1f4844ebf 100644 --- a/packages/features/bookings/lib/service/RegularBookingService.ts +++ b/packages/features/bookings/lib/service/RegularBookingService.ts @@ -66,12 +66,12 @@ import { extractBaseEmail } from "@calcom/lib/extract-base-email"; import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; import { HttpError } from "@calcom/lib/http-error"; -import logger from "@calcom/lib/logger"; import { criticalLogger } from "@calcom/lib/logger.server"; import { getPiiFreeCalendarEvent, getPiiFreeEventType } from "@calcom/lib/piiFreeData"; import { safeStringify } from "@calcom/lib/safeStringify"; import { getTranslation } from "@calcom/lib/server/i18n"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; +import { distributedTracing } from "@calcom/lib/tracing/factory"; import type { PrismaClient } from "@calcom/prisma"; import type { DestinationCalendar, Prisma, User, AssignmentReasonEnum } from "@calcom/prisma/client"; import { @@ -115,7 +115,6 @@ import { getSeatedBooking } from "../handleNewBooking/getSeatedBooking"; import { getVideoCallDetails } from "../handleNewBooking/getVideoCallDetails"; import { handleAppsStatus } from "../handleNewBooking/handleAppsStatus"; import { loadAndValidateUsers } from "../handleNewBooking/loadAndValidateUsers"; -import { createLoggerWithEventDetails } from "../handleNewBooking/logger"; import { getOriginalRescheduledBooking } from "../handleNewBooking/originalRescheduledBookingUtils"; import type { BookingType } from "../handleNewBooking/originalRescheduledBookingUtils"; import { scheduleNoShowTriggers } from "../handleNewBooking/scheduleNoShowTriggers"; @@ -126,7 +125,6 @@ import handleSeats from "../handleSeats/handleSeats"; import type { IBookingService } from "../interfaces/IBookingService"; const translator = short(); -const log = logger.getSubLogger({ prefix: ["[api] book:user"] }); type IsFixedAwareUserWithCredentials = Omit & { credentials: CredentialForCalendarService[]; @@ -479,8 +477,19 @@ async function handler( skipAvailabilityCheck = false, skipEventLimitsCheck = false, skipCalendarSyncTaskCreation = false, + traceContext: passedTraceContext, } = input; + const traceContext = passedTraceContext + ? passedTraceContext + : distributedTracing.createTrace("booking_creation"); + + const tracingLogger = distributedTracing.getTracingLogger(traceContext, { + eventTypeId: rawBookingData.eventTypeId, + userId: userId, + eventTypeSlug: rawBookingData.eventTypeSlug, + }); + const isPlatformBooking = !!platformClientId; const eventType = await getEventType({ @@ -527,8 +536,7 @@ async function handler( eventType, }); - const loggerWithEventDetails = createLoggerWithEventDetails(eventTypeId, reqBody.user, eventTypeSlug); - const emailsAndSmsHandler = new BookingEmailSmsHandler({ logger: loggerWithEventDetails }); + const emailsAndSmsHandler = new BookingEmailSmsHandler({ logger: tracingLogger }); try { await checkIfBookerEmailIsBlocked({ @@ -565,6 +573,7 @@ async function handler( throw new HttpError({ statusCode: 400, message: "email_verification_required", + data: { traceId: traceContext.traceId }, }); } @@ -574,12 +583,13 @@ async function handler( throw new HttpError({ statusCode: 400, message: "invalid_verification_code", + data: { traceId: traceContext.traceId }, }); } } if (isEventTypeLoggingEnabled({ eventTypeId, usernameOrTeamName: reqBody.user })) { - logger.settings.minLevel = 0; + tracingLogger.settings.minLevel = 0; } const fullName = getFullName(bookerName); @@ -587,12 +597,18 @@ async function handler( const tGuests = await getTranslation("en", "common"); const dynamicUserList = Array.isArray(reqBody.user) ? reqBody.user : getUsernameList(reqBody.user); - if (!eventType) throw new HttpError({ statusCode: 404, message: "event_type_not_found" }); + if (!eventType) + throw new HttpError({ + statusCode: 404, + message: "event_type_not_found", + data: { traceId: traceContext.traceId }, + }); if (eventType.seatsPerTimeSlot && eventType.recurringEvent) { throw new HttpError({ statusCode: 400, message: "recurring_event_seats_error", + data: { traceId: traceContext.traceId }, }); } @@ -669,7 +685,7 @@ async function handler( const shouldServeCache = false; - loggerWithEventDetails.info( + tracingLogger.info( `Booking eventType ${eventTypeId} started`, safeStringify({ reqBody: { @@ -704,7 +720,7 @@ async function handler( reqBody.timeZone, eventType, eventTimeZone, - loggerWithEventDetails + tracingLogger ); validateEventLength({ @@ -712,7 +728,7 @@ async function handler( reqBodyEnd: reqBody.end, eventTypeMultipleDuration: eventType.metadata?.multipleDuration, eventTypeLength: eventType.length, - logger: loggerWithEventDetails, + logger: tracingLogger, }); const contactOwnerFromReq = reqBody.teamMemberEmail ?? null; @@ -731,7 +747,11 @@ async function handler( if (routedTeamMemberIds) { //routingFormResponseId could be 0 for dry run. So, we just avoid undefined value if (routingFormResponseId === undefined) { - throw new HttpError({ statusCode: 400, message: "Missing routingFormResponseId" }); + throw new HttpError({ + statusCode: 400, + message: "Missing routingFormResponseId", + data: { traceId: traceContext.traceId }, + }); } routingFormResponse = await deps.prismaClient.app_RoutingForms_FormResponse.findUnique({ where: { @@ -757,7 +777,7 @@ async function handler( eventType, eventTypeId, dynamicUserList, - logger: loggerWithEventDetails, + logger: tracingLogger, routedTeamMemberIds: routedTeamMemberIds ?? null, contactOwnerEmail, rescheduleUid: reqBody.rescheduleUid || null, @@ -868,7 +888,7 @@ async function handler( timeZone: reqBody.timeZone, originalRescheduledBooking: originalRescheduledBooking ?? null, }, - loggerWithEventDetails, + tracingLogger, shouldServeCache ); } @@ -883,7 +903,7 @@ async function handler( timeZone: reqBody.timeZone, originalRescheduledBooking, }, - loggerWithEventDetails, + tracingLogger, shouldServeCache ); } @@ -902,7 +922,7 @@ async function handler( timeZone: reqBody.timeZone, originalRescheduledBooking, }, - loggerWithEventDetails, + tracingLogger, shouldServeCache ); } else { @@ -910,7 +930,7 @@ async function handler( } } catch { if (additionalFallbackRRUsers.length) { - loggerWithEventDetails.debug( + tracingLogger.debug( "Qualified users not available, check for fallback users", safeStringify({ qualifiedRRUsers: qualifiedRRUsers.map((user) => user.id), @@ -931,14 +951,14 @@ async function handler( timeZone: reqBody.timeZone, originalRescheduledBooking, }, - loggerWithEventDetails, + tracingLogger, shouldServeCache ); } else { availableUsers = [...additionalFallbackRRUsers, ...fixedUsers] as IsFixedAwareUser[]; } } else { - loggerWithEventDetails.debug( + tracingLogger.debug( "Qualified users not available, no fallback users", safeStringify({ qualifiedRRUsers: qualifiedRRUsers.map((user) => user.id), @@ -967,7 +987,7 @@ async function handler( const notAvailableLuckyUsers: typeof users = []; - loggerWithEventDetails.debug( + tracingLogger.debug( "Computed available users", safeStringify({ availableUsers: availableUsers.map((user) => user.id), @@ -1044,7 +1064,7 @@ async function handler( timeZone: reqBody.timeZone, originalRescheduledBooking, }, - loggerWithEventDetails, + tracingLogger, shouldServeCache ); } @@ -1054,7 +1074,7 @@ async function handler( luckUserFound = true; } catch { notAvailableLuckyUsers.push(newLuckyUser); - loggerWithEventDetails.info( + tracingLogger.info( `Round robin host ${newLuckyUser.name} not available for first two slots. Trying to find another host.` ); } @@ -1119,7 +1139,7 @@ async function handler( } if (users.length === 0 && eventType.schedulingType === SchedulingType.ROUND_ROBIN) { - loggerWithEventDetails.error(`No available users found for round robin event.`); + tracingLogger.error(`No available users found for round robin event.`); throw new Error(ErrorCode.RoundRobinHostsUnavailableForBooking); } @@ -1233,7 +1253,7 @@ async function handler( }, [] as Invitee); if (guestsRemoved.length > 0) { - log.info("Removed guests from the booking", guestsRemoved); + tracingLogger.info("Removed guests from the booking", guestsRemoved); } const seed = `${organizerUser.username}:${dayjs(reqBody.start).utc().format()}:${new Date().getTime()}`; @@ -1248,8 +1268,8 @@ async function handler( } : getLocationValueForDB(locationBodyString, eventType.locations); - log.info("locationBodyString", locationBodyString); - log.info("event type locations", eventType.locations); + tracingLogger.info("locationBodyString", locationBodyString); + tracingLogger.info("event type locations", eventType.locations); const customInputs = getCustomInputsResponses(reqBody, eventType.customInputs); const attendeesList = [...invitee, ...guests]; @@ -1663,7 +1683,7 @@ async function handler( let booking: CreatedBooking | null = null; - loggerWithEventDetails.debug( + tracingLogger.debug( "Going to create booking in DB now", safeStringify({ organizerUser: organizerUser.id, @@ -1824,13 +1844,13 @@ async function handler( } } catch (_err) { const err = getErrorFromUnknown(_err); - loggerWithEventDetails.error( - `Booking ${eventTypeId} failed`, - "Error when saving booking to db", - err.message - ); + tracingLogger.error(`Booking ${eventTypeId} failed`, "Error when saving booking to db", err.message); if (err.code === "P2002") { - throw new HttpError({ statusCode: 409, message: ErrorCode.BookingConflict }); + throw new HttpError({ + statusCode: 409, + message: ErrorCode.BookingConflict, + data: { traceId: traceContext.traceId }, + }); } throw err; } @@ -1847,7 +1867,7 @@ async function handler( // this is the actual rescheduling logic if (!eventType.seatsPerTimeSlot && originalRescheduledBooking?.uid) { - log.silly("Rescheduling booking", originalRescheduledBooking.uid); + tracingLogger.silly("Rescheduling booking", originalRescheduledBooking.uid); // cancel workflow reminders from previous rescheduled booking await WorkflowRepository.deleteAllWorkflowReminders(originalRescheduledBooking.workflowReminders); @@ -1878,7 +1898,7 @@ async function handler( { ...originalRescheduledBooking.user, credentials: refreshedOriginalHostCredentials }, apps ); - log.debug("RescheduleOrganizerChanged: Deleting Event and Meeting for previous booking"); + tracingLogger.debug("RescheduleOrganizerChanged: Deleting Event and Meeting for previous booking"); // Create deletion event with original host's organizer info and original booking properties const deletionEvent = { ...evt, @@ -1949,7 +1969,7 @@ async function handler( message: "Booking Rescheduling failed", }; - loggerWithEventDetails.error( + tracingLogger.error( `EventManager.reschedule failure in some of the integrations ${organizerUser.username}`, safeStringify({ error, results }) ); @@ -1972,7 +1992,7 @@ async function handler( const googleCalResult = results[googleCalIndex]; if (!googleCalResult) { - loggerWithEventDetails.warn("Google Calendar not installed but using Google Meet as location"); + tracingLogger.warn("Google Calendar not installed but using Google Meet as location"); results.push({ ...googleMeetResult, success: false, @@ -2074,9 +2094,10 @@ async function handler( const error = { errorCode: "BookingCreatingMeetingFailed", message: "Booking failed", + data: { traceId: traceContext.traceId }, }; - loggerWithEventDetails.error( + tracingLogger.error( `EventManager.create failure in some of the integrations ${organizerUser.username}`, safeStringify({ error, results }) ); @@ -2101,7 +2122,7 @@ async function handler( const googleCalResult = results[googleCalIndex]; if (!googleCalResult) { - loggerWithEventDetails.warn("Google Calendar not installed but using Google Meet as location"); + tracingLogger.warn("Google Calendar not installed but using Google Meet as location"); results.push({ ...googleMeetResult, success: false, @@ -2179,7 +2200,7 @@ async function handler( } } else { // If isConfirmedByDefault is false, then booking can't be considered ACCEPTED and thus EventManager has no role to play. Booking is created as PENDING - loggerWithEventDetails.debug( + tracingLogger.debug( `EventManager doesn't need to create or reschedule event for booking ${organizerUser.username}`, safeStringify({ calEvent: getPiiFreeCalendarEvent(evt), @@ -2196,7 +2217,7 @@ async function handler( !!booking; if (!isConfirmedByDefault && noEmail !== true && !bookingRequiresPayment) { - loggerWithEventDetails.debug( + tracingLogger.debug( `Emails: Booking ${organizerUser.username} requires confirmation, sending request emails`, safeStringify({ calEvent: getPiiFreeCalendarEvent(evt), @@ -2236,7 +2257,7 @@ async function handler( const bookingRescheduledPayload = bookingCreatedPayload; const bookingEventHandler = new BookingEventHandlerService({ - log: loggerWithEventDetails, + log: tracingLogger, hashedLinkService: deps.hashedLinkService, }); @@ -2268,7 +2289,7 @@ async function handler( }; if (bookingRequiresPayment) { - loggerWithEventDetails.debug(`Booking ${organizerUser.username} requires payment`); + tracingLogger.debug(`Booking ${organizerUser.username} requires payment`); // Load credentials.app.categories const credentialPaymentAppCategories = await deps.prismaClient.credential.findMany({ where: { @@ -2295,7 +2316,11 @@ async function handler( }); if (!eventTypePaymentAppCredential) { - throw new HttpError({ statusCode: 400, message: "Missing payment credentials" }); + throw new HttpError({ + statusCode: 400, + message: "Missing payment credentials", + data: { traceId: traceContext.traceId }, + }); } // Convert type of eventTypePaymentAppCredential to appId: EventTypeAppList @@ -2363,7 +2388,7 @@ async function handler( }); } } catch (error) { - loggerWithEventDetails.error( + tracingLogger.error( "Error while scheduling workflow reminders for booking payment initiated", JSON.stringify({ error }) ); @@ -2394,7 +2419,7 @@ async function handler( }; } - loggerWithEventDetails.debug(`Booking ${organizerUser.username} completed`); + tracingLogger.debug(`Booking ${organizerUser.username} completed`); // We are here so, booking doesn't require payment and booking is also created in DB already, through createBooking call if (isConfirmedByDefault) { @@ -2456,7 +2481,7 @@ async function handler( const failures = scheduledTriggerResults.filter((result) => result.status === "rejected"); if (failures.length > 0) { - loggerWithEventDetails.error( + tracingLogger.error( "Error while scheduling or canceling webhook triggers", safeStringify({ errors: failures.map((f) => f.reason), @@ -2504,7 +2529,7 @@ async function handler( }); } } catch (error) { - loggerWithEventDetails.error("Error while creating booking references", JSON.stringify({ error })); + tracingLogger.error("Error while creating booking references", JSON.stringify({ error })); } const evtWithMetadata = { @@ -2540,7 +2565,7 @@ async function handler( isRescheduleEvent: !!rescheduleUid, }); } catch (error) { - loggerWithEventDetails.error("Error while scheduling workflow reminders", JSON.stringify({ error })); + tracingLogger.error("Error while scheduling workflow reminders", JSON.stringify({ error })); } try { @@ -2561,7 +2586,7 @@ async function handler( }); } } catch (error) { - loggerWithEventDetails.error("Error while scheduling no show triggers", JSON.stringify({ error })); + tracingLogger.error("Error while scheduling no show triggers", JSON.stringify({ error })); } if (!isDryRun) { diff --git a/packages/lib/server/defaultResponder.test.ts b/packages/lib/server/defaultResponder.test.ts index 24926ad316..8f4b967241 100644 --- a/packages/lib/server/defaultResponder.test.ts +++ b/packages/lib/server/defaultResponder.test.ts @@ -11,7 +11,11 @@ describe("defaultResponder", () => { it("should call res.json when response is still writable and result is not null", async () => { const f = vi.fn().mockResolvedValue({}); const req = {} as NextApiRequest; - const res = { json: vi.fn(), writableEnded: false } as unknown as NextApiResponse; + const res = { + json: vi.fn(), + writableEnded: false, + setHeader: vi.fn(), + } as unknown as NextApiResponse; await defaultResponder(f)(req, res); expect(res.json).toHaveBeenCalled(); }); @@ -19,21 +23,33 @@ describe("defaultResponder", () => { it("should not call res.json when response is not writable", async () => { const f = vi.fn().mockResolvedValue({}); const req = {} as NextApiRequest; - const res = { json: vi.fn(), writableEnded: true } as unknown as NextApiResponse; + const res = { + json: vi.fn(), + writableEnded: true, + setHeader: vi.fn(), + } as unknown as NextApiResponse; await defaultResponder(f)(req, res); expect(res.json).not.toHaveBeenCalled(); }); it("should respond with status code 409 for NoAvailableUsersFound", async () => { const f = vi.fn().mockRejectedValue(new Error(ErrorCode.NoAvailableUsersFound)); const req = {} as NextApiRequest; - const res = { status: vi.fn().mockReturnThis(), json: vi.fn() } as unknown as NextApiResponse; + const res = { + status: vi.fn().mockReturnThis(), + json: vi.fn(), + setHeader: vi.fn(), + } as unknown as NextApiResponse; await defaultResponder(f)(req, res); expect(res.status).toHaveBeenCalledWith(409); }); it("Rate limit should respond with a 429 status code", async () => { const f = vi.fn().mockRejectedValue(new TRPCError({ code: "TOO_MANY_REQUESTS" })); const req = {} as NextApiRequest; - const res = { status: vi.fn().mockReturnThis(), json: vi.fn() } as unknown as NextApiResponse; + const res = { + status: vi.fn().mockReturnThis(), + json: vi.fn(), + setHeader: vi.fn(), + } as unknown as NextApiResponse; await defaultResponder(f)(req, res); expect(res.status).toHaveBeenCalledWith(429); }); diff --git a/packages/lib/server/defaultResponder.ts b/packages/lib/server/defaultResponder.ts index 2fc4de3045..454a1e7673 100644 --- a/packages/lib/server/defaultResponder.ts +++ b/packages/lib/server/defaultResponder.ts @@ -3,11 +3,19 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { TRPCError } from "@trpc/server"; import { getHTTPStatusCodeFromError } from "@trpc/server/http"; +import { type TraceContext } from "@calcom/lib/tracing"; +import { TracedError } from "@calcom/lib/tracing/error"; +import { distributedTracing } from "@calcom/lib/tracing/factory"; + import { HttpError } from "../http-error"; import { getServerErrorFromUnknown } from "./getServerErrorFromUnknown"; import { performance } from "./perfObserver"; -type Handle = (req: NextApiRequest, res: NextApiResponse) => Promise; +export interface TracedRequest extends NextApiRequest { + traceContext: TraceContext; +} + +type Handle = (req: TracedRequest, res: NextApiResponse) => Promise; /** Allows us to get type inference from API handler responses */ export function defaultResponder( @@ -17,22 +25,37 @@ export function defaultResponder( ) { return async (req: NextApiRequest, res: NextApiResponse) => { let ok = false; + const operation = endpointRoute ? endpointRoute.replace(/^\//, "").replace(/\//g, "_") : "api_request"; + const traceContext = distributedTracing.createTrace(operation, { + meta: { + method: req.method || "", + url: req.url || "", + }, + }); + const tracingLogger = distributedTracing.getTracingLogger(traceContext); + + const tracedReq = req as TracedRequest; + tracedReq.traceContext = traceContext; + try { performance.mark("Start"); let result: T | undefined; if (process.env.NODE_ENV === "development" || !endpointRoute) { - result = await f(req, res); + result = await f(tracedReq, res); } else { const { wrapApiHandlerWithSentry } = await import("@sentry/nextjs"); - result = await wrapApiHandlerWithSentry(f, endpointRoute)(req, res); + result = await wrapApiHandlerWithSentry(f, endpointRoute)(tracedReq, res); } ok = true; if (result && !res.writableEnded) { + res.setHeader("X-Trace-Id", traceContext.traceId); return res.json(result); } } catch (err) { + tracingLogger.error(`${operation} request failed`, { error: err }); + const tracedError = TracedError.createFromError(err, traceContext); let error: HttpError; if (err instanceof TRPCError) { const statusCode = getHTTPStatusCodeFromError(err); @@ -44,8 +67,9 @@ export function defaultResponder( if (!(error.statusCode >= 400 && error.statusCode < 500)) { console.error(error); const { captureException } = await import("@sentry/nextjs"); - captureException(error); + captureException(tracedError); } + res.setHeader("X-Trace-Id", tracedError.traceId); return res .status(error.statusCode) .json({ message: error.message, url: error.url, method: error.method, data: error?.data || null }); diff --git a/packages/lib/tracing/index.ts b/packages/lib/tracing/index.ts index 73750e72e3..b5431252e6 100644 --- a/packages/lib/tracing/index.ts +++ b/packages/lib/tracing/index.ts @@ -6,7 +6,7 @@ export interface TraceContext { parentSpanId?: string; operation: string; // So that we don't violate open closed principle, we allow meta to be added to the trace context - meta?: Record; + meta?: Record; } export interface IdGenerator { @@ -31,7 +31,7 @@ export class DistributedTracing { createSpan( parentContext: TraceContext, operation: string, - additionalMeta?: Record + additionalMeta?: Record ): TraceContext { const mergedMeta = { ...parentContext.meta, @@ -47,7 +47,15 @@ export class DistributedTracing { }; } - getTracingLogger(context: TraceContext) { + getTracingLogger( + context: TraceContext, + additionalMeta?: Record + ) { + const mergedMeta = { + ...context.meta, + ...additionalMeta, + }; + const prefixes = [ "distributed-trace", `trace:${context.traceId}`, @@ -55,8 +63,8 @@ export class DistributedTracing { `op:${context.operation}`, ]; - if (context.meta) { - Object.entries(context.meta).forEach(([key, value]) => { + if (mergedMeta) { + Object.entries(mergedMeta).forEach(([key, value]) => { if (value !== undefined && value !== null) { prefixes.push(`${key}:${value}`); } @@ -85,7 +93,10 @@ export class DistributedTracing { }; } - updateTrace(traceContext: TraceContext, additionalMeta?: Record): TraceContext { + updateTrace( + traceContext: TraceContext, + additionalMeta?: Record + ): TraceContext { return { ...traceContext, meta: { diff --git a/packages/trpc/server/createContext.ts b/packages/trpc/server/createContext.ts index 7b3affa5dd..007d0347a2 100644 --- a/packages/trpc/server/createContext.ts +++ b/packages/trpc/server/createContext.ts @@ -5,7 +5,9 @@ import type { serverSideTranslations } from "next-i18next/serverSideTranslations import { getLocale } from "@calcom/features/auth/lib/getLocale"; import getIP from "@calcom/lib/getIP"; -import prisma, { readonlyPrisma } from "@calcom/prisma"; +import type { TraceContext } from "@calcom/lib/tracing"; +import { distributedTracing } from "@calcom/lib/tracing/factory"; +import { prisma, readonlyPrisma } from "@calcom/prisma"; import type { SelectedCalendar, User as PrismaUser } from "@calcom/prisma/client"; import type { CreateNextContextOptions } from "@trpc/server/adapters/next"; @@ -50,6 +52,7 @@ export type GetSessionFn = export type InnerContext = CreateInnerContextOptions & { prisma: typeof prisma; insightsDb: typeof readonlyPrisma; + traceContext: TraceContext; }; /** @@ -62,10 +65,17 @@ export type InnerContext = CreateInnerContextOptions & { * @see https://trpc.io/docs/context#inner-and-outer-context */ export async function createContextInner(opts: CreateInnerContextOptions): Promise { + const traceContext = distributedTracing.createTrace("trpc_request", { + meta: { + userId: opts.session?.user?.id?.toString() || "anonymous", + }, + }); + return { prisma, insightsDb: readonlyPrisma, ...opts, + traceContext, }; } @@ -87,7 +97,7 @@ export const createContext = async ( // This type may not be accurate if this request is coming from SSG init but they both should satisfy the requirements of getIP. // TODO: @sean - figure out a way to make getIP be happy with trpc req. params const sourceIp = getIP(req as NextApiRequest); - const session = !!sessionGetter ? await sessionGetter({ req, res }) : null; + const session = sessionGetter ? await sessionGetter({ req, res }) : null; const contextInner = await createContextInner({ locale, session, sourceIp }); return { ...contextInner,