diff --git a/src/index.spec.ts b/src/index.spec.ts index eba64684..b78fc802 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -24,6 +24,7 @@ import { TraceSource } from "./trace/trace-context-service"; import { inflateSync } from "zlib"; import { MetricsListener } from "./metrics/listener"; import { SpanOptions, TracerWrapper } from "./trace/tracer-wrapper"; +import fs from "fs"; jest.mock("./metrics/enhanced-metrics"); @@ -623,3 +624,78 @@ describe("emitTelemetryOnErrorOutsideHandler", () => { expect(mockedStartSpan).toBeCalledTimes(0); }); }); + +describe("detectDuplicateInstallations", () => { + jest.mock("fs/promises"); + + let fsAccessMock: jest.SpyInstance; + let logWarningMock: jest.SpyInstance; + + beforeEach(() => { + jest.resetAllMocks(); + fsAccessMock = jest.spyOn(fs.promises, "access"); + logWarningMock = jest.spyOn(require("./utils"), "logWarning").mockImplementation(() => {}); + }); + + it("should log warning when duplicate installations are detected", async () => { + // Mock fs.promises.access to simulate both paths exist + fsAccessMock.mockResolvedValue(undefined); // undefined (no error) = path exists + + await datadog( + async () => { + /* empty */ + }, + { forceWrap: true }, + )(); + expect(logWarningMock).toHaveBeenCalledWith(expect.stringContaining("Detected duplicate installations")); + }); + + it("should not log warning when only layer installation exists", async () => { + // Simulate layerPath exists, localPath does not exist + fsAccessMock.mockImplementation((path: string) => { + if (path.includes("/opt/nodejs")) { + return Promise.resolve(); // Exists + } + return Promise.reject(new Error("ENOENT")); // Does not exist + }); + + await datadog( + async () => { + /* empty */ + }, + { forceWrap: true }, + )(); + expect(logWarningMock).not.toHaveBeenCalled(); + }); + + it("should not log warning when only local installation exists", async () => { + // Simulate localPath exists, layerPath does not exist + fsAccessMock.mockImplementation((path: string) => { + if (path.includes("/opt/nodejs")) { + return Promise.reject(new Error("ENOENT")); // Does not exist + } + return Promise.resolve(); // Exists + }); + + await datadog( + async () => { + /* empty */ + }, + { forceWrap: true }, + )(); + expect(logWarningMock).not.toHaveBeenCalled(); + }); + + it("should not log warning when neither installation exists", async () => { + // Simulate neither path exists + fsAccessMock.mockRejectedValue(new Error("ENOENT")); // Does not exist + + await datadog( + async () => { + /* empty */ + }, + { forceWrap: true }, + )(); + expect(logWarningMock).not.toHaveBeenCalled(); + }); +}); diff --git a/src/index.ts b/src/index.ts index 1d401e4f..266612fd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,11 +21,14 @@ import { setSandboxInit, setLogger, setLogLevel, + logWarning, } from "./utils"; import { getEnhancedMetricTags } from "./metrics/enhanced-metrics"; import { DatadogTraceHeaders } from "./trace/context/extractor"; import { SpanWrapper } from "./trace/span-wrapper"; import { SpanOptions, TracerWrapper } from "./trace/tracer-wrapper"; +import path from "path"; +import fs from "fs"; // Backwards-compatible export, TODO deprecate in next major export { DatadogTraceHeaders as TraceHeaders } from "./trace/context/extractor"; @@ -104,6 +107,9 @@ export const _metricsQueue: MetricsQueue = new MetricsQueue(); let currentMetricsListener: MetricsListener | undefined; let currentTraceListener: TraceListener | undefined; +const LAMBDA_LAYER_PATH = "/opt/nodejs/node_modules/datadog-lambda-js"; +const LAMBDA_LIBRARY_PATH = path.join(process.cwd(), "node_modules/datadog-lambda-js"); + if (getEnvValue(coldStartTracingEnvVar, "true").toLowerCase() === "true") { subscribeToDC(); } @@ -131,6 +137,19 @@ export function datadog( const traceListener = new TraceListener(finalConfig); + // Check for duplicate installations of the Lambda library + detectDuplicateInstallations() + .then((duplicateFound) => { + if (duplicateFound) { + logWarning( + `Detected duplicate installations of datadog-lambda-js. This can cause: (1) increased cold start times, (2) broken metrics, and (3) other unexpected behavior. Please use either the Lambda layer version or the package in node_modules, but not both. See: https://docs.datadoghq.com/serverless/aws_lambda/installation/nodejs/?tab=custom`, + ); + } + }) + .catch(() => { + logDebug("Failed to check for duplicate installations."); + }); + // Only wrap the handler once unless forced const _ddWrappedKey = "_ddWrapped"; if ((handler as any)[_ddWrappedKey] !== undefined && !finalConfig.forceWrap) { @@ -478,3 +497,26 @@ export async function emitTelemetryOnErrorOutsideHandler( await metricsListener.onCompleteInvocation(); } } + +async function detectDuplicateInstallations() { + try { + const checkPathExistsAsync = async (libraryPath: string): Promise => { + try { + await fs.promises.access(libraryPath); + return true; + } catch { + return false; + } + }; + + const [layerExists, localExists] = await Promise.all([ + checkPathExistsAsync(LAMBDA_LAYER_PATH), + checkPathExistsAsync(LAMBDA_LIBRARY_PATH), + ]); + + return layerExists && localExists; + } catch (err) { + logDebug("Failed to check for duplicate installations."); + return false; + } +}