Skip to content

Commit

Permalink
feat(sdk): aws.Function.context() for lambda functions (#6424)
Browse files Browse the repository at this point in the history
Closes #6412

## Checklist

- [x] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted)
- [x] Description explains motivation and solution
- [x] Tests added (always)
- [ ] Docs updated (only required for features)
- [ ] Added `pr/e2e-full` label if this feature requires end-to-end testing

*By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*.
  • Loading branch information
Chriscbr committed May 21, 2024
1 parent 47810aa commit a93ede6
Show file tree
Hide file tree
Showing 14 changed files with 426 additions and 23 deletions.
135 changes: 135 additions & 0 deletions docs/docs/04-standard-library/aws/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1053,10 +1053,23 @@ new aws.Function();

| **Name** | **Description** |
| --- | --- |
| <code><a href="#@winglang/sdk.aws.Function.context">context</a></code> | Returns the current Lambda invocation context, if the host is an AWS Lambda. |
| <code><a href="#@winglang/sdk.aws.Function.from">from</a></code> | If the inflight host is an AWS Lambda, return a helper interface for working with it. |

---

##### `context` <a name="context" id="@winglang/sdk.aws.Function.context"></a>

```wing
bring aws;
aws.Function.context();
```

Returns the current Lambda invocation context, if the host is an AWS Lambda.

> [https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html)
##### `from` <a name="from" id="@winglang/sdk.aws.Function.from"></a>

```wing
Expand Down Expand Up @@ -2001,6 +2014,128 @@ AWS Bucket name.

---

### ILambdaContext <a name="ILambdaContext" id="@winglang/sdk.aws.ILambdaContext"></a>

- *Implemented By:* <a href="#@winglang/sdk.aws.ILambdaContext">ILambdaContext</a>

The AWS Lambda context object.

#### Methods <a name="Methods" id="Methods"></a>

| **Name** | **Description** |
| --- | --- |
| <code><a href="#@winglang/sdk.aws.ILambdaContext.remainingTimeInMillis">remainingTimeInMillis</a></code> | Returns the number of milliseconds left before the execution times out. |

---

##### `remainingTimeInMillis` <a name="remainingTimeInMillis" id="@winglang/sdk.aws.ILambdaContext.remainingTimeInMillis"></a>

```wing
remainingTimeInMillis(): num
```

Returns the number of milliseconds left before the execution times out.

#### Properties <a name="Properties" id="Properties"></a>

| **Name** | **Type** | **Description** |
| --- | --- | --- |
| <code><a href="#@winglang/sdk.aws.ILambdaContext.property.awsRequestId">awsRequestId</a></code> | <code>str</code> | The identifier of the invocation request. |
| <code><a href="#@winglang/sdk.aws.ILambdaContext.property.functionName">functionName</a></code> | <code>str</code> | The name of the Lambda function. |
| <code><a href="#@winglang/sdk.aws.ILambdaContext.property.functionVersion">functionVersion</a></code> | <code>str</code> | The version of the function. |
| <code><a href="#@winglang/sdk.aws.ILambdaContext.property.invokedFunctionArn">invokedFunctionArn</a></code> | <code>str</code> | The Amazon Resource Name (ARN) that's used to invoke the function. |
| <code><a href="#@winglang/sdk.aws.ILambdaContext.property.logGroupName">logGroupName</a></code> | <code>str</code> | The log group for the function. |
| <code><a href="#@winglang/sdk.aws.ILambdaContext.property.logStreamName">logStreamName</a></code> | <code>str</code> | The log stream for the function instance. |
| <code><a href="#@winglang/sdk.aws.ILambdaContext.property.memoryLimitInMB">memoryLimitInMB</a></code> | <code>str</code> | The amount of memory that's allocated for the function. |

---

##### `awsRequestId`<sup>Required</sup> <a name="awsRequestId" id="@winglang/sdk.aws.ILambdaContext.property.awsRequestId"></a>

```wing
awsRequestId: str;
```

- *Type:* str

The identifier of the invocation request.

---

##### `functionName`<sup>Required</sup> <a name="functionName" id="@winglang/sdk.aws.ILambdaContext.property.functionName"></a>

```wing
functionName: str;
```

- *Type:* str

The name of the Lambda function.

---

##### `functionVersion`<sup>Required</sup> <a name="functionVersion" id="@winglang/sdk.aws.ILambdaContext.property.functionVersion"></a>

```wing
functionVersion: str;
```

- *Type:* str

The version of the function.

---

##### `invokedFunctionArn`<sup>Required</sup> <a name="invokedFunctionArn" id="@winglang/sdk.aws.ILambdaContext.property.invokedFunctionArn"></a>

```wing
invokedFunctionArn: str;
```

- *Type:* str

The Amazon Resource Name (ARN) that's used to invoke the function.

Indicates if the invoker specified a version number or alias.

---

##### `logGroupName`<sup>Required</sup> <a name="logGroupName" id="@winglang/sdk.aws.ILambdaContext.property.logGroupName"></a>

```wing
logGroupName: str;
```

- *Type:* str

The log group for the function.

---

##### `logStreamName`<sup>Required</sup> <a name="logStreamName" id="@winglang/sdk.aws.ILambdaContext.property.logStreamName"></a>

```wing
logStreamName: str;
```

- *Type:* str

The log stream for the function instance.

---

##### `memoryLimitInMB`<sup>Required</sup> <a name="memoryLimitInMB" id="@winglang/sdk.aws.ILambdaContext.property.memoryLimitInMB"></a>

```wing
memoryLimitInMB: str;
```

- *Type:* str

The amount of memory that's allocated for the function.

---

## Enums <a name="Enums" id="Enums"></a>

### Effect <a name="Effect" id="@winglang/sdk.aws.Effect"></a>
Expand Down
19 changes: 19 additions & 0 deletions docs/docs/04-standard-library/cloud/function.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,25 @@ if let lambdaFn = aws.Function.from(f) {
}
```
To access the AWS Lambda context object, you can use the `aws.Function` class as shown below.
```ts playground
bring aws;
bring cloud;

let f = new cloud.Function(inflight () => {
if let ctx = aws.Function.context() {
log(ctx.logGroupName); // prints the log group name
log(ctx.logStreamName); // prints the log stream name

let remainingTime = ctx.remainingTimeInMillis();
assert(remainingTime > 0);
}
});
```
The `context()` method returns `nil` when ran on non-AWS targets.
### Azure (`tf-azure`)
The Azure implementation of `cloud.Function` uses [Azure Functions](https://azure.microsoft.com/en-us/products/functions).
Expand Down
41 changes: 39 additions & 2 deletions examples/tests/sdk_tests/function/aws-function.test.w
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
bring cloud;
bring aws;
bring cloud;
bring expect;
bring util;

let target = util.env("WING_TARGET");
Expand Down Expand Up @@ -35,4 +36,40 @@ test "validates the AWS Function" {
// If the test is not on AWS, it should not fail, so I am returning true.
assert(true);
}
}
}

let fn = new cloud.Function(inflight (msg: str?) => {
if msg == "error" {
throw "fake error";
}

if let ctx = aws.Function.context() {
log(Json.stringify(ctx));
expect.equal(ctx.functionVersion, "$LATEST");

let remainingTime = ctx.remainingTimeInMillis();
assert(remainingTime > 0);
} else {
if target == "tf-aws" || target == "awscdk" {
expect.fail("Expected to have a context object");
}
}

return msg;
}) as "FunctionAccessingContext";

test "can access lambda context" {
let result = fn.invoke("hello");
expect.equal(result, "hello");

let result2 = fn.invoke("hello2");
expect.equal(result2, "hello2");

let var msg = "";
try {
fn.invoke("error");
} catch err {
msg = err;
}
expect.ok(msg.contains("fake error"), "Expected fake error message");
}
19 changes: 17 additions & 2 deletions libs/awscdk/src/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ import { Construct, IConstruct } from "constructs";
import { cloud, std, core } from "@winglang/sdk";
import { NotImplementedError } from "@winglang/sdk/lib/core/errors";
import { createBundle } from "@winglang/sdk/lib/shared/bundling";
import { IAwsFunction, NetworkConfig, PolicyStatement, externalLibraries } from "@winglang/sdk/lib/shared-aws";
import {
IAwsFunction,
NetworkConfig,
PolicyStatement,
externalLibraries,
} from "@winglang/sdk/lib/shared-aws";
import { makeAwsLambdaHandler } from "@winglang/sdk/lib/shared-aws/function-util";
import { resolve } from "path";
import { renameSync, rmSync, writeFileSync } from "fs";
import { App } from "./app";
Expand Down Expand Up @@ -201,7 +207,9 @@ export class Function

public addNetwork(config: NetworkConfig): void {
config;
throw new Error("The AWS CDK platform provider does not support adding network configurations to AWS Lambda functions at the moment.");
throw new Error(
"The AWS CDK platform provider does not support adding network configurations to AWS Lambda functions at the moment."
);
}

private envName(): string {
Expand All @@ -219,4 +227,11 @@ export class Function
public get functionName(): string {
return this.function.functionName;
}

/**
* @internal
*/
protected _getCodeLines(handler: cloud.IFunctionHandler): string[] {
return makeAwsLambdaHandler(handler);
}
}
3 changes: 0 additions & 3 deletions libs/wingc/src/type_check/jsii_importer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -583,9 +583,6 @@ impl<'a> JsiiImporter<'a> {
if let Some(properties) = jsii_interface.properties() {
for p in properties {
debug!("Found property {} with type {:?}", p.name.green(), p.type_);
if member_phase == Phase::Inflight {
todo!("No support for inflight properties yet");
}
let base_wing_type = self.type_ref_to_wing_type(&p.type_);
let is_optional = if let Some(true) = p.optional { true } else { false };
let is_static = if let Some(true) = p.static_ { true } else { false };
Expand Down
19 changes: 19 additions & 0 deletions libs/wingsdk/src/cloud/function.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,25 @@ if let lambdaFn = aws.Function.from(f) {
}
```
To access the AWS Lambda context object, you can use the `aws.Function` class as shown below.
```ts playground
bring aws;
bring cloud;

let f = new cloud.Function(inflight () => {
if let ctx = aws.Function.context() {
log(ctx.logGroupName); // prints the log group name
log(ctx.logStreamName); // prints the log stream name

let remainingTime = ctx.remainingTimeInMillis();
assert(remainingTime > 0);
}
});
```
The `context()` method returns `nil` when ran on non-AWS targets.
### Azure (`tf-azure`)
The Azure implementation of `cloud.Function` uses [Azure Functions](https://azure.microsoft.com/en-us/products/functions).
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion libs/wingsdk/src/expect/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from "./assert";
export * from "./expect";
34 changes: 34 additions & 0 deletions libs/wingsdk/src/shared-aws/function-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as cloud from "../cloud";

export function makeAwsLambdaHandler(
handler: cloud.IFunctionHandler
): string[] {
const inflightClient = handler._toInflight();
const lines = new Array<string>();
const client = "$handler";

lines.push('"use strict";');
lines.push(`var ${client} = undefined;`);
lines.push("exports.handler = async function(event, context) {");
lines.push(" try {");
lines.push(" if (globalThis.$awsLambdaContext === undefined) {");
lines.push(" globalThis.$awsLambdaContext = context;");
lines.push(` ${client} = ${client} ?? (${inflightClient});`);
lines.push(" } else {");
lines.push(" throw new Error(");
lines.push(" 'An AWS Lambda context object was already defined.'");
lines.push(" );");
lines.push(" }");

// important: we're calling handle() within a try block, but there's no catch block
// because we want to let the error propagate to the AWS Lambda runtime
lines.push(
` return await ${client}.handle(event === null ? undefined : event);`
);
lines.push(" } finally {");
lines.push(" globalThis.$awsLambdaContext = undefined;");
lines.push(" }");
lines.push("};");

return lines;
}
11 changes: 11 additions & 0 deletions libs/wingsdk/src/shared-aws/function.inflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,21 @@ import {
LogType,
} from "@aws-sdk/client-lambda";
import { fromUtf8, toUtf8 } from "@smithy/util-utf8";
import { ILambdaContext } from "./function";
import { IFunctionClient } from "../cloud";
import { LogLevel, Trace, TraceType } from "../std";

export class FunctionClient implements IFunctionClient {
public static async context(): Promise<ILambdaContext | undefined> {
const obj = (globalThis as any).$awsLambdaContext;
if (!obj) {
return undefined;
}
// workaround for the fact that JSII doesn't allow methods to start with "get"
obj.remainingTimeInMillis = obj.getRemainingTimeInMillis;
return obj;
}

constructor(
private readonly functionArn: string,
private readonly constructPath: string,
Expand Down
Loading

0 comments on commit a93ede6

Please sign in to comment.