Skip to content

Commit 3a372b8

Browse files
ramisa2108Ramisa Alam
and
Ramisa Alam
authored
feat: add tenant-id to invoke context and structured log messages (#128)
* feat: add tenant id to invoke context * test: add unit tests for next invocation through rapid client * feat: add tenant-id to structured log message --------- Co-authored-by: Ramisa Alam <[email protected]>
1 parent c9a3619 commit 3a372b8

9 files changed

+242
-4
lines changed

deps/aws-lambda-cpp-0.2.8.tar.gz

-4.1 KB
Binary file not shown.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
diff --git a/include/aws/lambda-runtime/runtime.h b/include/aws/lambda-runtime/runtime.h
2+
index f7cb8ef..833b69d 100644
3+
--- a/include/aws/lambda-runtime/runtime.h
4+
+++ b/include/aws/lambda-runtime/runtime.h
5+
@@ -56,6 +56,11 @@ struct invocation_request {
6+
*/
7+
std::string function_arn;
8+
9+
+ /**
10+
+ * The Tenant ID of the current invocation.
11+
+ */
12+
+ std::string tenant_id;
13+
+
14+
/**
15+
* Function execution deadline counted in milliseconds since the Unix epoch.
16+
*/
17+
diff --git a/src/runtime.cpp b/src/runtime.cpp
18+
index d1e655f..bcc217d 100644
19+
--- a/src/runtime.cpp
20+
+++ b/src/runtime.cpp
21+
@@ -40,6 +40,7 @@ static constexpr auto CLIENT_CONTEXT_HEADER = "lambda-runtime-client-context";
22+
static constexpr auto COGNITO_IDENTITY_HEADER = "lambda-runtime-cognito-identity";
23+
static constexpr auto DEADLINE_MS_HEADER = "lambda-runtime-deadline-ms";
24+
static constexpr auto FUNCTION_ARN_HEADER = "lambda-runtime-invoked-function-arn";
25+
+static constexpr auto TENANT_ID_HEADER = "lambda-runtime-aws-tenant-id";
26+
27+
enum Endpoints {
28+
INIT,
29+
@@ -296,6 +297,11 @@ runtime::next_outcome runtime::get_next()
30+
req.function_arn = std::move(out).get_result();
31+
}
32+
33+
+ out = resp.get_header(TENANT_ID_HEADER);
34+
+ if (out.is_success()) {
35+
+ req.tenant_id = std::move(out).get_result();
36+
+ }
37+
+
38+
out = resp.get_header(DEADLINE_MS_HEADER);
39+
if (out.is_success()) {
40+
auto const& deadline_string = std::move(out).get_result();

scripts/update_dependencies.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ wget -c https://github.com/awslabs/aws-lambda-cpp/archive/refs/tags/v$AWS_LAMBDA
2929
# Apply patches
3030
(
3131
cd aws-lambda-cpp-$AWS_LAMBDA_CPP_RELEASE && \
32-
patch -p1 < ../patches/aws-lambda-cpp-add-xray-response.patch
32+
patch -p1 < ../patches/aws-lambda-cpp-add-xray-response.patch && \
33+
patch -p1 < ../patches/aws-lambda-cpp-add-tenant-id.patch
3334
)
3435

3536
# Pack again and remove the folder

src/InvokeContext.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
'use strict';
1010

1111
const assert = require('assert').strict;
12-
let { setCurrentRequestId } = require('./LogPatch');
12+
let { setCurrentRequestId, setCurrentTenantId } = require('./LogPatch');
1313

1414
const INVOKE_HEADER = {
1515
ClientContext: 'lambda-runtime-client-context',
@@ -18,6 +18,7 @@ const INVOKE_HEADER = {
1818
AWSRequestId: 'lambda-runtime-aws-request-id',
1919
DeadlineMs: 'lambda-runtime-deadline-ms',
2020
XRayTrace: 'lambda-runtime-trace-id',
21+
TenantId: 'lambda-runtime-aws-tenant-id',
2122
};
2223

2324
module.exports = class InvokeContext {
@@ -34,11 +35,19 @@ module.exports = class InvokeContext {
3435
return id;
3536
}
3637

38+
/**
39+
* The tenantId for this request.
40+
*/
41+
get tenantId() {
42+
return this.headers[INVOKE_HEADER.TenantId];
43+
}
44+
3745
/**
3846
* Push relevant invoke data into the logging context.
3947
*/
4048
updateLoggingContext() {
4149
setCurrentRequestId(this.invokeId);
50+
setCurrentTenantId(this.tenantId);
4251
}
4352

4453
/**
@@ -91,6 +100,7 @@ module.exports = class InvokeContext {
91100
),
92101
invokedFunctionArn: this.headers[INVOKE_HEADER.ARN],
93102
awsRequestId: this.headers[INVOKE_HEADER.AWSRequestId],
103+
tenantId: this.headers[INVOKE_HEADER.TenantId],
94104
getRemainingTimeInMillis: function () {
95105
return deadline - Date.now();
96106
},

src/LogPatch.js

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,23 @@ const jsonErrorReplacer = (_, value) => {
2828
return value;
2929
};
3030

31-
function formatJsonMessage(requestId, timestamp, level, ...messageParams) {
31+
function formatJsonMessage(
32+
requestId,
33+
timestamp,
34+
level,
35+
tenantId,
36+
...messageParams
37+
) {
3238
let result = {
3339
timestamp: timestamp,
3440
level: level.name,
3541
requestId: requestId,
3642
};
3743

44+
if (tenantId != undefined && tenantId != null) {
45+
result.tenantId = tenantId;
46+
}
47+
3848
if (messageParams.length === 1) {
3949
result.message = messageParams[0];
4050
try {
@@ -65,6 +75,13 @@ let _currentRequestId = {
6575
set: (id) => (global[REQUEST_ID_SYMBOL] = id),
6676
};
6777

78+
/* Use a unique symbol to provide global access without risk of name clashes. */
79+
const TENANT_ID_SYMBOL = Symbol.for('aws.lambda.runtime.tenantId');
80+
let _currentTenantId = {
81+
get: () => global[TENANT_ID_SYMBOL],
82+
set: (id) => (global[TENANT_ID_SYMBOL] = id),
83+
};
84+
6885
/**
6986
* Write logs to stdout.
7087
*/
@@ -82,7 +99,15 @@ let logTextToStdout = (level, message, ...params) => {
8299
let logJsonToStdout = (level, message, ...params) => {
83100
let time = new Date().toISOString();
84101
let requestId = _currentRequestId.get();
85-
let line = formatJsonMessage(requestId, time, level, message, ...params);
102+
let tenantId = _currentTenantId.get();
103+
let line = formatJsonMessage(
104+
requestId,
105+
time,
106+
level,
107+
tenantId,
108+
message,
109+
...params,
110+
);
86111
line = line.replace(/\n/g, '\r');
87112
process.stdout.write(line + '\n');
88113
};
@@ -125,10 +150,12 @@ let logJsonToFd = function (logTarget) {
125150
let date = new Date();
126151
let time = date.toISOString();
127152
let requestId = _currentRequestId.get();
153+
let tenantId = _currentTenantId.get();
128154
let enrichedMessage = formatJsonMessage(
129155
requestId,
130156
time,
131157
level,
158+
tenantId,
132159
message,
133160
...params,
134161
);
@@ -239,6 +266,7 @@ let _patchConsole = () => {
239266

240267
module.exports = {
241268
setCurrentRequestId: _currentRequestId.set,
269+
setCurrentTenantId: _currentTenantId.set,
242270
patchConsole: _patchConsole,
243271
structuredConsole: structuredConsole,
244272
};

src/rapid-client.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ class RuntimeApiNextPromiseWorker : public Napi::AsyncWorker {
8282
Napi::String::New(env, "lambda-runtime-cognito-identity"),
8383
Napi::String::New(env, response.cognito_identity.c_str()));
8484
}
85+
if (response.tenant_id != "") {
86+
headers.Set(
87+
Napi::String::New(env, "lambda-runtime-aws-tenant-id"),
88+
Napi::String::New(env, response.tenant_id.c_str()));
89+
}
8590

8691
auto ret = Napi::Object::New(env);
8792
ret.Set(Napi::String::New(env, "bodyJson"), response_data);

test/unit/InvokeContextTest.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,32 @@ describe('Getting remaining invoke time', () => {
3535
remainingTime.should.lessThanOrEqual(1000);
3636
});
3737
});
38+
39+
describe('Verifying tenant id', () => {
40+
it('should return undefined if tenant id is not set', () => {
41+
let ctx = new InvokeContext({});
42+
43+
(ctx._headerData().tenantId === undefined).should.be.true();
44+
});
45+
it('should return undefined if tenant id is set to undefined', () => {
46+
let ctx = new InvokeContext({
47+
'lambda-runtime-aws-tenant-id': undefined,
48+
});
49+
50+
(ctx._headerData().tenantId === undefined).should.be.true();
51+
});
52+
it('should return empty if tenant id is set to empty string', () => {
53+
let ctx = new InvokeContext({
54+
'lambda-runtime-aws-tenant-id': '',
55+
});
56+
57+
(ctx._headerData().tenantId === '').should.be.true();
58+
});
59+
it('should return the same id if a valid tenant id is set', () => {
60+
let ctx = new InvokeContext({
61+
'lambda-runtime-aws-tenant-id': 'blue',
62+
});
63+
64+
ctx._headerData().tenantId.should.equal('blue');
65+
});
66+
});

test/unit/LogPatchTest.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,9 +415,38 @@ describe('The multiline log patch', () => {
415415
);
416416
receivedMessage.should.have.property('level', logFunctions[fIdx][1]);
417417
receivedMessage.should.have.property('requestId', EXPECTED_ID);
418+
receivedMessage.should.not.have.property('tenantId');
418419
}
419420
});
420421

422+
it('should format messages with tenant id as json correctly', () => {
423+
const EXPECTED_TENANT_ID = 'tenantId';
424+
LogPatch.setCurrentTenantId(EXPECTED_TENANT_ID);
425+
426+
for (let fIdx = 0; fIdx < logFunctions.length; fIdx++) {
427+
logFunctions[fIdx][0]('structured logging with tenant id');
428+
let receivedMessage = telemetryTarget.readLine(
429+
logFunctions[fIdx][1],
430+
'JSON',
431+
);
432+
receivedMessage = JSON.parse(receivedMessage);
433+
434+
receivedMessage.should.have.property('timestamp');
435+
let receivedTime = new Date(receivedMessage.timestamp);
436+
let now = new Date();
437+
assert(now >= receivedTime && now - receivedTime <= 1000);
438+
439+
receivedMessage.should.have.property(
440+
'message',
441+
'structured logging with tenant id',
442+
);
443+
receivedMessage.should.have.property('level', logFunctions[fIdx][1]);
444+
receivedMessage.should.have.property('requestId', EXPECTED_ID);
445+
receivedMessage.should.have.property('tenantId', EXPECTED_TENANT_ID);
446+
}
447+
LogPatch.setCurrentTenantId(undefined);
448+
});
449+
421450
it('should filter messages correctly', () => {
422451
const loglevelSettings = [
423452
undefined,

test/unit/RAPIDClientTest.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,23 @@ class NoOpNativeHttp {
4747
}
4848
}
4949

50+
class MockNativeClient {
51+
constructor(response) {
52+
this.response = response;
53+
this.called = false;
54+
this.shouldThrowError = false;
55+
}
56+
57+
next() {
58+
this.called = true;
59+
if (this.shouldThrowError) {
60+
return Promise.reject(new Error('Failed to get next invocation'));
61+
} else {
62+
return Promise.resolve(this.response);
63+
}
64+
}
65+
}
66+
5067
class EvilError extends Error {
5168
get name() {
5269
throw 'gotcha';
@@ -115,3 +132,82 @@ describe('invalid request id works', () => {
115132
});
116133
});
117134
});
135+
136+
describe('next invocation with native client works', () => {
137+
it('should call the native client next() method', async () => {
138+
const mockNative = new MockNativeClient({
139+
bodyJson: '',
140+
headers: {
141+
'lambda-runtime-aws-request-id': 'test-request-id',
142+
},
143+
});
144+
const client = new RAPIDClient('notUsed:1337', undefined, mockNative);
145+
client.useAlternativeClient = false;
146+
147+
await client.nextInvocation();
148+
// verify native client was called
149+
mockNative.called.should.be.true();
150+
});
151+
it('should parse all required headers', async () => {
152+
const mockResponse = {
153+
bodyJson: '{"message":"Hello from Lambda!"}',
154+
headers: {
155+
'lambda-runtime-aws-request-id': 'test-request-id',
156+
'lambda-runtime-deadline-ms': 1619712000000,
157+
'lambda-runtime-trace-id': 'test-trace-id',
158+
'lambda-runtime-invoked-function-arn': 'test-function-arn',
159+
'lambda-runtime-client-context': '{"client":{"app_title":"MyApp"}}',
160+
'lambda-runtime-cognito-identity':
161+
'{"identityId":"id123","identityPoolId":"pool123"}',
162+
'lambda-runtime-aws-tenant-id': 'test-tenant-id',
163+
},
164+
};
165+
166+
const mockNative = new MockNativeClient(mockResponse);
167+
const client = new RAPIDClient('notUsed:1337', undefined, mockNative);
168+
169+
client.useAlternativeClient = false;
170+
const response = await client.nextInvocation();
171+
172+
// Verify all headers are present
173+
response.headers.should.have.property(
174+
'lambda-runtime-aws-request-id',
175+
'test-request-id',
176+
);
177+
response.headers.should.have.property(
178+
'lambda-runtime-deadline-ms',
179+
1619712000000,
180+
);
181+
response.headers.should.have.property(
182+
'lambda-runtime-trace-id',
183+
'test-trace-id',
184+
);
185+
response.headers.should.have.property(
186+
'lambda-runtime-invoked-function-arn',
187+
'test-function-arn',
188+
);
189+
response.headers.should.have.property(
190+
'lambda-runtime-client-context',
191+
'{"client":{"app_title":"MyApp"}}',
192+
);
193+
response.headers.should.have.property(
194+
'lambda-runtime-cognito-identity',
195+
'{"identityId":"id123","identityPoolId":"pool123"}',
196+
);
197+
response.headers.should.have.property(
198+
'lambda-runtime-aws-tenant-id',
199+
'test-tenant-id',
200+
);
201+
// Verify body is correctly passed through
202+
response.bodyJson.should.equal('{"message":"Hello from Lambda!"}');
203+
});
204+
it('should handle native client errors', async () => {
205+
const nativeClient = new MockNativeClient({});
206+
nativeClient.shouldThrowError = true;
207+
208+
const client = new RAPIDClient('localhost:8080', null, nativeClient);
209+
client.useAlternativeClient = false;
210+
211+
await client.nextInvocation().should.be.rejectedWith('Failed to get next invocation');
212+
});
213+
});

0 commit comments

Comments
 (0)