Skip to content

Commit

Permalink
Merge pull request #26 from DataDog/darcy.rayner/support-es-modules
Browse files Browse the repository at this point in the history
Add support for webpack/es modules
  • Loading branch information
DarcyRaynerDD authored Feb 24, 2020
2 parents 3a5bf43 + 9a9d9eb commit 2b1be5e
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 84 deletions.
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,36 @@ custom:

You can use your own version of those libraries by setting 'addLayers' to false in the datadog configuration block. Just make sure to bundle those libaries with your Lambda functions.

### How do I use this with serverless-webpack?

Make sure serverless-datadog is above the serverless-webpack entry in your serverless.yml

```yaml
plugins:
- serverless-plugin-datadog
- serverless-webpack
```

When using serverless webpack, the plugin will assume you are using es6 module format. If that's not the case, you can manually configure `nodeModuleType`.

```yaml
custom:
datadog:
nodeModuleType: "node" # 'typescript' | 'es6'
```

If you have the addLayers option enabled, you may also want to add 'datadog-lambda-js' and 'dd-trace' to the [externals](https://webpack.js.org/configuration/externals/) section of your webpack config.

### How do I use this with serverless-typescript?

Make sure serverless-datadog is above the serverless-typescript entry in your serverless.yml. The plugin will detect automatically .ts files.

```yaml
plugins:
- serverless-plugin-datadog
- serverless-typescript
```

## Opening Issues

If you encounter a bug with this package, we want to hear about it. Before opening a new issue, search the existing issues to avoid duplicates.
Expand All @@ -96,4 +126,4 @@ If you find an issue with this package and have a fix, please feel free to open

Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.

This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2019 Datadog, Inc.
This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2019 Datadog, Inc.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "serverless-plugin-datadog",
"version": "0.15.0",
"version": "0.16.0",
"description": "Serverless plugin to automatically instrument python and node functions with datadog tracing",
"main": "dist/index.js",
"repository": "https://github.com/DataDog/serverless-plugin-datadog",
Expand Down
3 changes: 3 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export interface Configuration {
logLevel: string;
// Whether the log forwarder integration is enabled by default
flushMetricsToLogs: boolean;
// When set, the plugin will always write wrapper handlers in the given format. Otherwise, will try
// to infer the handler type either from the extension, or presence of webpack.
nodeModuleType?: "es6" | "node" | "typescript";
}

const apiKeyEnvVar = "DD_API_KEY";
Expand Down
15 changes: 14 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,20 @@ module.exports = class ServerlessPlugin {
const config = getConfig(this.serverless.service);
setEnvConfiguration(config, this.serverless.service);
const defaultRuntime = this.serverless.service.provider.runtime;
const handlers = findHandlers(this.serverless.service, defaultRuntime);
let defaultNodeRuntime: RuntimeType.NODE | RuntimeType.NODE_ES6 | RuntimeType.NODE_TS | undefined;
switch (config.nodeModuleType) {
case "es6":
defaultNodeRuntime = RuntimeType.NODE_ES6;
break;
case "typescript":
defaultNodeRuntime = RuntimeType.NODE_TS;
break;
case "node":
defaultNodeRuntime = RuntimeType.NODE;
break;
}

const handlers = findHandlers(this.serverless.service, defaultRuntime, defaultNodeRuntime);
if (config.addLayers) {
this.serverless.cli.log("Adding Lambda Layers to functions");
this.debugLogHandlers(handlers);
Expand Down
134 changes: 116 additions & 18 deletions src/layer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,64 +10,71 @@ import { HandlerInfo, LayerJSON, RuntimeType, applyLayers, findHandlers } from "

import { FunctionDefinition } from "serverless";
import Service from "serverless/classes/Service";
import fs from "fs";
import mock from "mock-fs";

function createMockService(region: string, funcs: { [funcName: string]: Partial<FunctionDefinition> }): Service {
const service: Partial<Service> & { functions: any } = {
function createMockService(
region: string,
funcs: { [funcName: string]: Partial<FunctionDefinition> },
plugins?: string[],
): Service {
const service: Partial<Service> & { functions: any; plugins: any } = {
provider: { region } as any,
getAllFunctionsNames: () => Object.keys(funcs),
getFunction: (name) => funcs[name] as FunctionDefinition,
functions: funcs as any,
plugins,
};
return service as Service;
}

describe("findHandlers", () => {
it("finds all node and python layers with matching layers", () => {
const mockService = createMockService("us-east-1", {
"func-a": { runtime: "nodejs8.10" },
"func-b": { runtime: "go1.10" },
"func-c": { runtime: "nodejs10.x" },
"func-d": { runtime: "python2.7" },
"func-e": { runtime: "python3.6" },
"func-f": { runtime: "python3.7" },
"func-g": { runtime: "python3.8" },
"func-h": { runtime: "nodejs12.x" },
"func-a": { handler: "myfile.handler", runtime: "nodejs8.10" },
"func-b": { handler: "myfile.handler", runtime: "go1.10" },
"func-c": { handler: "myfile.handler", runtime: "nodejs10.x" },
"func-d": { handler: "myfile.handler", runtime: "python2.7" },
"func-e": { handler: "myfile.handler", runtime: "python3.6" },
"func-f": { handler: "myfile.handler", runtime: "python3.7" },
"func-g": { handler: "myfile.handler", runtime: "python3.8" },
"func-h": { handler: "myfile.handler", runtime: "nodejs12.x" },
});

const result = findHandlers(mockService);
expect(result).toMatchObject([
{
handler: { runtime: "nodejs8.10" },
handler: { handler: "myfile.handler", runtime: "nodejs8.10" },
type: RuntimeType.NODE,
runtime: "nodejs8.10",
},
{
handler: { runtime: "go1.10" },
handler: { handler: "myfile.handler", runtime: "go1.10" },
type: RuntimeType.UNSUPPORTED,
runtime: "go1.10",
},
{
handler: { runtime: "nodejs10.x" },
handler: { handler: "myfile.handler", runtime: "nodejs10.x" },
type: RuntimeType.NODE,
runtime: "nodejs10.x",
},
{
handler: { runtime: "python2.7" },
handler: { handler: "myfile.handler", runtime: "python2.7" },
type: RuntimeType.PYTHON,
runtime: "python2.7",
},
{
handler: { runtime: "python3.6" },
handler: { handler: "myfile.handler", runtime: "python3.6" },
type: RuntimeType.PYTHON,
runtime: "python3.6",
},
{
handler: { runtime: "python3.7" },
handler: { handler: "myfile.handler", runtime: "python3.7" },
type: RuntimeType.PYTHON,
runtime: "python3.7",
},
{
handler: { runtime: "python3.8" },
handler: { handler: "myfile.handler", runtime: "python3.8" },
type: RuntimeType.PYTHON,
runtime: "python3.8",
},
Expand All @@ -80,7 +87,7 @@ describe("findHandlers", () => {
});
it("uses the global runtime when one isn't specified", () => {
const mockService = createMockService("us-east-1", {
"func-a": {},
"func-a": { handler: "myfile.handler" },
});
const result = findHandlers(mockService, "nodejs8.10");
expect(result).toMatchObject([
Expand All @@ -91,6 +98,97 @@ describe("findHandlers", () => {
},
]);
});

it("uses typescript runtime when ts file is detected", () => {
mock({
"mylambda.ts": "",
});
const mockService = createMockService("us-east-1", {
"func-a": { handler: "mylambda.handler", runtime: "nodejs8.10" },
});
const result = findHandlers(mockService, "nodejs8.10");

expect(result).toMatchObject([
{
handler: { handler: "mylambda.handler", runtime: "nodejs8.10" },
type: RuntimeType.NODE_TS,
runtime: "nodejs8.10",
},
]);
});
it("uses es6 runtime when es.js file is detected", () => {
mock({
"mylambda.es.js": "",
});
const mockService = createMockService("us-east-1", {
"func-a": { handler: "mylambda.handler", runtime: "nodejs8.10" },
});
const result = findHandlers(mockService, "nodejs8.10");

expect(result).toMatchObject([
{
handler: { handler: "mylambda.handler", runtime: "nodejs8.10" },
type: RuntimeType.NODE_ES6,
runtime: "nodejs8.10",
},
]);
});
it("uses es6 runtime when .mjs file is detected", () => {
mock({
"mylambda.mjs": "",
});
const mockService = createMockService("us-east-1", {
"func-a": { handler: "mylambda.handler", runtime: "nodejs8.10" },
});
const result = findHandlers(mockService, "nodejs8.10");

expect(result).toMatchObject([
{
handler: { handler: "mylambda.handler", runtime: "nodejs8.10" },
type: RuntimeType.NODE_ES6,
runtime: "nodejs8.10",
},
]);
});
it("uses default node runtime when provided", () => {
mock({
"mylambda.js": "",
});
const mockService = createMockService("us-east-1", {
"func-a": { handler: "mylambda.handler", runtime: "nodejs8.10" },
});
const result = findHandlers(mockService, "nodejs8.10", RuntimeType.NODE_TS);

expect(result).toMatchObject([
{
handler: { handler: "mylambda.handler", runtime: "nodejs8.10" },
type: RuntimeType.NODE_TS,
runtime: "nodejs8.10",
},
]);
});
it("uses es6 runtime by default when webpack detected", () => {
mock({
"mylambda.js": "",
});
const plugins = ["serverless-plugin-datadog", "serverless-webpack"];
const mockService = createMockService(
"us-east-1",
{
"func-a": { handler: "mylambda.handler", runtime: "nodejs8.10" },
},
plugins,
);
const result = findHandlers(mockService, "nodejs8.10");

expect(result).toMatchObject([
{
handler: { handler: "mylambda.handler", runtime: "nodejs8.10" },
type: RuntimeType.NODE_ES6,
runtime: "nodejs8.10",
},
]);
});
});

describe("applyLayers", () => {
Expand Down
43 changes: 40 additions & 3 deletions src/layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019 Datadog, Inc.
*/

import fs from "fs";
import { FunctionDefinition } from "serverless";
import Service from "serverless/classes/Service";
import { getHandlerPath } from "./util";

export enum RuntimeType {
NODE,
NODE_TS,
NODE_ES6,
PYTHON,
UNSUPPORTED,
}
Expand Down Expand Up @@ -43,7 +45,11 @@ export const runtimeLookup: { [key: string]: RuntimeType } = {
"python3.8": RuntimeType.PYTHON,
};

export function findHandlers(service: Service, defaultRuntime?: string): HandlerInfo[] {
export function findHandlers(
service: Service,
defaultRuntime?: string,
defaultNodeRuntime?: RuntimeType.NODE_ES6 | RuntimeType.NODE_TS | RuntimeType.NODE,
): HandlerInfo[] {
const funcs = (service as any).functions as { [key: string]: FunctionDefinition };

return Object.entries(funcs)
Expand All @@ -53,7 +59,30 @@ export function findHandlers(service: Service, defaultRuntime?: string): Handler
runtime = defaultRuntime;
}
if (runtime !== undefined && runtime in runtimeLookup) {
return { type: runtimeLookup[runtime], runtime, name, handler } as HandlerInfo;
const handlerInfo = { type: runtimeLookup[runtime], runtime, name, handler } as HandlerInfo;
if (handlerInfo.type === RuntimeType.NODE) {
const handlerPath = getHandlerPath(handlerInfo);
if (handlerPath === undefined) {
return;
}

if (defaultNodeRuntime === undefined) {
if (
fs.existsSync(`./${handlerPath.filename}.es.js`) ||
fs.existsSync(`./${handlerPath.filename}.mjs`) ||
hasWebpackPlugin(service)
) {
handlerInfo.type = RuntimeType.NODE_ES6;
}
if (fs.existsSync(`./${handlerPath.filename}.ts`)) {
handlerInfo.type = RuntimeType.NODE_TS;
}
} else {
handlerInfo.type = defaultNodeRuntime;
}
}

return handlerInfo;
}
return { type: RuntimeType.UNSUPPORTED, runtime, name, handler } as HandlerInfo;
})
Expand Down Expand Up @@ -94,3 +123,11 @@ function getLayers(handler: HandlerInfo) {
function setLayers(handler: HandlerInfo, layers: string[]) {
(handler.handler as any).layers = layers;
}

function hasWebpackPlugin(service: Service) {
const plugins: string[] | undefined = (service as any).plugins;
if (plugins === undefined) {
return false;
}
return plugins.find((plugin) => plugin === "serverless-webpack") !== undefined;
}
14 changes: 14 additions & 0 deletions src/templates/node-es6-template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed
* under the Apache License Version 2.0.
*
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019 Datadog, Inc.
*/

export function es6Template(filePath: string, method: string) {
return `/* eslint-disable */
const { datadog } = require("datadog-lambda-js");
import * as original from "../${filePath}";
export const ${method} = datadog(original.${method});`;
}
12 changes: 12 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import * as fs from "fs";

import { HandlerInfo } from "layer";
import { promisify } from "util";

const exists = promisify(fs.exists);
Expand All @@ -32,3 +33,14 @@ export async function removeDirectory(path: string) {
await rmdir(path);
}
}

export function getHandlerPath(handlerInfo: HandlerInfo) {
const handlerfile = handlerInfo.handler.handler;
const parts = handlerfile.split(".");
if (parts.length < 2) {
return;
}
const method = parts[parts.length - 1];
const filename = parts.slice(0, -1).join(".");
return { method, filename };
}
Loading

0 comments on commit 2b1be5e

Please sign in to comment.