Skip to content

Commit 03d25e1

Browse files
authored
Merge branch 'main' into romain.marcadier/log-to-debug
2 parents 3a9fdf3 + 7e278cd commit 03d25e1

19 files changed

Lines changed: 555 additions & 28 deletions

File tree

.gitlab/templates/pipeline.yaml.tpl

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,49 @@ signed layer bundle:
324324
- mkdir -p datadog_extension-signed-bundle-${CI_JOB_ID}
325325
- cp .layers/datadog_extension-*.zip datadog_extension-signed-bundle-${CI_JOB_ID}
326326

327+
# Integration Tests - Build Java Lambda function
328+
build java lambda:
329+
stage: integration-tests
330+
image: registry.ddbuild.io/images/docker:27.3.1
331+
tags: ["docker-in-docker:arm64"]
332+
rules:
333+
- when: on_success
334+
needs: []
335+
artifacts:
336+
expire_in: 1 hour
337+
paths:
338+
- integration-tests/lambda/base-java/target/
339+
script:
340+
- cd integration-tests/lambda/base-java
341+
- docker run --rm --platform linux/arm64
342+
-v "$(pwd)":/workspace
343+
-w /workspace
344+
maven:3.9-eclipse-temurin-21-alpine
345+
mvn clean package
346+
347+
# Integration Tests - Build .NET Lambda function
348+
build dotnet lambda:
349+
stage: integration-tests
350+
image: registry.ddbuild.io/images/docker:27.3.1
351+
tags: ["docker-in-docker:arm64"]
352+
rules:
353+
- when: on_success
354+
needs: []
355+
artifacts:
356+
expire_in: 1 hour
357+
paths:
358+
- integration-tests/lambda/base-dotnet/bin/
359+
script:
360+
- cd integration-tests/lambda/base-dotnet
361+
- docker run --rm --platform linux/arm64
362+
-v "$(pwd)":/workspace
363+
-w /workspace
364+
mcr.microsoft.com/dotnet/sdk:8.0-alpine
365+
sh -c "apk add --no-cache zip &&
366+
dotnet tool install -g Amazon.Lambda.Tools || true &&
367+
export PATH=\"\$PATH:/root/.dotnet/tools\" &&
368+
dotnet lambda package -o bin/function.zip --function-architecture arm64"
369+
327370
# Integration Tests - Publish arm64 layer with integration test prefix
328371
publish integration layer (arm64):
329372
stage: integration-tests
@@ -362,6 +405,11 @@ integration-deploy:
362405
- when: on_success
363406
needs:
364407
- publish integration layer (arm64)
408+
- build java lambda
409+
- build dotnet lambda
410+
dependencies:
411+
- build java lambda
412+
- build dotnet lambda
365413
variables:
366414
IDENTIFIER: ${CI_COMMIT_SHORT_SHA}
367415
AWS_DEFAULT_REGION: us-east-1
@@ -418,35 +466,55 @@ integration-cleanup-stacks:
418466
stage: integration-tests
419467
tags: ["arch:amd64"]
420468
image: ${CI_DOCKER_TARGET_IMAGE}:${CI_DOCKER_TARGET_VERSION}
421-
when: always
422469
rules:
423470
- when: always
424471
needs:
425-
- integration-test
472+
- job: integration-test
473+
optional: false
426474
variables:
427475
IDENTIFIER: ${CI_COMMIT_SHORT_SHA}
428476
{{ with $environment := (ds "environments").environments.sandbox }}
429477
before_script:
430478
- EXTERNAL_ID_NAME={{ $environment.external_id }} ROLE_TO_ASSUME={{ $environment.role_to_assume }} AWS_ACCOUNT={{ $environment.account }} source .gitlab/scripts/get_secrets.sh
431-
- curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
432-
- apt-get install -y nodejs
433-
- cd integration-tests
434-
- npm ci
435479
{{ end }}
436480
script:
437481
- echo "Destroying CDK stacks with identifier ${IDENTIFIER}..."
438-
- npx cdk destroy "integ-$IDENTIFIER-*" --force || echo "Failed to destroy some stacks, but continuing..."
482+
- |
483+
# Find all stacks matching the pattern using CloudFormation API
484+
STACKS=$(aws cloudformation list-stacks \
485+
--stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE UPDATE_ROLLBACK_COMPLETE \
486+
--query "StackSummaries[?starts_with(StackName, 'integ-${IDENTIFIER}-')].StackName" \
487+
--output text --region us-east-1)
488+
489+
if [ -z "$STACKS" ]; then
490+
echo "No stacks found matching pattern integ-${IDENTIFIER}-*"
491+
else
492+
echo "Found stacks to delete: ${STACKS}"
493+
for STACK in $STACKS; do
494+
echo "Deleting stack ${STACK}..."
495+
aws cloudformation delete-stack --stack-name "${STACK}" --region us-east-1 || echo "Failed to delete ${STACK}, continuing..."
496+
done
497+
498+
# Wait for all deletions to complete
499+
echo "Waiting for stack deletions to complete..."
500+
for STACK in $STACKS; do
501+
echo "Waiting for ${STACK}..."
502+
aws cloudformation wait stack-delete-complete --stack-name "${STACK}" --region us-east-1 || echo "Stack ${STACK} deletion did not complete cleanly, continuing..."
503+
done
504+
505+
echo "All stacks deleted successfully"
506+
fi
439507

440508
# Integration Tests - Cleanup layer
441509
integration-cleanup-layer:
442510
stage: integration-tests
443511
tags: ["arch:amd64"]
444512
image: ${CI_DOCKER_TARGET_IMAGE}:${CI_DOCKER_TARGET_VERSION}
445-
when: always
446513
rules:
447514
- when: always
448515
needs:
449-
- integration-cleanup-stacks
516+
- job: integration-cleanup-stacks
517+
optional: false
450518
variables:
451519
IDENTIFIER: ${CI_COMMIT_SHORT_SHA}
452520
{{ with $environment := (ds "environments").environments.sandbox }}
@@ -456,7 +524,7 @@ integration-cleanup-layer:
456524
script:
457525
- echo "Deleting integration test layer with identifier ${IDENTIFIER}..."
458526
- |
459-
LAYER_NAME="Datadog-Extension-${IDENTIFIER}"
527+
LAYER_NAME="Datadog-Extension-ARM-${IDENTIFIER}"
460528
echo "Looking for layer: ${LAYER_NAME}"
461529

462530
# Get all versions of the layer

bottlecap/src/appsec/processor/response.rs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use std::collections::HashMap;
22
use std::io::Cursor;
33

4-
use serde::Deserialize;
4+
use serde::{Deserialize, Deserializer};
55

66
use crate::appsec::processor::InvocationPayload;
7-
use crate::lifecycle::invocation::triggers::{body::Body, lowercase_key};
7+
use crate::lifecycle::invocation::triggers::body::Body;
88

99
/// The expected payload of a response. This is different from trigger to trigger.
1010
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -50,9 +50,9 @@ impl Default for ExpectedResponseFormat {
5050
#[serde(rename_all = "camelCase")]
5151
struct ApiGatewayResponse {
5252
status_code: i64,
53-
#[serde(deserialize_with = "lowercase_key", default)]
53+
#[serde(deserialize_with = "nullable_lowercase_key", default)]
5454
headers: HashMap<String, String>,
55-
#[serde(deserialize_with = "lowercase_key", default)]
55+
#[serde(deserialize_with = "nullable_lowercase_key", default)]
5656
multi_value_headers: HashMap<String, Vec<String>>,
5757
#[serde(flatten)]
5858
body: Body,
@@ -101,3 +101,39 @@ impl InvocationPayload for RawPayload {
101101
Some(Box::new(Cursor::new(&self.data)))
102102
}
103103
}
104+
105+
fn nullable_lowercase_key<'de, D, V>(deserializer: D) -> Result<HashMap<String, V>, D::Error>
106+
where
107+
D: Deserializer<'de>,
108+
V: Deserialize<'de>,
109+
{
110+
let Some(map) = Option::<HashMap<String, V>>::deserialize(deserializer)? else {
111+
return Ok(HashMap::default());
112+
};
113+
Ok(map
114+
.into_iter()
115+
.map(|(key, value)| (key.to_lowercase(), value))
116+
.collect())
117+
}
118+
119+
#[cfg(test)]
120+
mod test {
121+
use super::*;
122+
123+
#[test]
124+
fn test_null_fields_in_apigw_response() {
125+
let response = r#"{
126+
"statusCode": 0,
127+
"headers": null,
128+
"multiValueHeaders": null,
129+
"body": null
130+
}"#;
131+
let response = ExpectedResponseFormat::ApiGatewayResponse
132+
.parse(response.as_bytes())
133+
.expect("response should have parsed cleanly")
134+
.expect("response should have been Some");
135+
assert!(response.response_body().is_none());
136+
assert!(response.response_headers_no_cookies().is_empty());
137+
assert_eq!(response.response_status_code(), Some(0));
138+
}
139+
}

bottlecap/src/logs/agent.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::sync::Arc;
2+
use std::time::{Duration, Instant};
23
use tokio::sync::mpsc::{self, Sender};
34
use tokio_util::sync::CancellationToken;
45
use tracing::debug;
@@ -9,6 +10,8 @@ use crate::logs::{aggregator_service::AggregatorHandle, processor::LogsProcessor
910
use crate::tags;
1011
use crate::{LAMBDA_RUNTIME_SLUG, config};
1112

13+
const DRAIN_LOG_INTERVAL: Duration = Duration::from_millis(100);
14+
1215
#[allow(clippy::module_name_repetitions)]
1316
pub struct LogsAgent {
1417
rx: mpsc::Receiver<TelemetryEvent>,
@@ -57,6 +60,7 @@ impl LogsAgent {
5760
debug!("LOGS_AGENT | Received shutdown signal, draining remaining events");
5861

5962
// Drain remaining events
63+
let mut last_drain_log_time = Instant::now().checked_sub(DRAIN_LOG_INTERVAL).expect("Failed to subtract interval from now");
6064
'drain_logs_loop: loop {
6165
match self.rx.try_recv() {
6266
Ok(event) => {
@@ -68,7 +72,12 @@ impl LogsAgent {
6872
},
6973
// Empty signals there are still outstanding senders
7074
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
71-
debug!("LOGS_AGENT | No more events to process but still have senders, continuing to drain...");
75+
// Log at most once every 100ms to avoid spamming the logs
76+
let now = Instant::now();
77+
if now.duration_since(last_drain_log_time) >= DRAIN_LOG_INTERVAL {
78+
debug!("LOGS_AGENT | No more events to process but still have senders, continuing to drain...");
79+
last_drain_log_time = now;
80+
}
7281
},
7382
}
7483
}

integration-tests/README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,40 @@ The general flow is:
88
3. Wait for data to propagate to Datadog.
99
4. Call Datadog to get telemetry data and check the data based on test requirements.
1010

11-
For simplicity, integraiton tests are setup to only test against ARM runtimes.
11+
For simplicity, integration tests are setup to only test against ARM runtimes.
12+
13+
## Test Suites
14+
15+
### Base Tests
16+
17+
The base test suite provides basic functionality tests across all supported Lambda runtimes. Also serves as an example for other tests.
18+
19+
The base tests verify the extension can:
20+
- Collect and forward logs to Datadog
21+
- Generate and send traces with proper span structure
22+
- Detect cold starts
23+
24+
**Test Coverage:**
25+
- Lambda invocation succeeds (200 status code)
26+
- "Hello world!" log message is sent to Datadog
27+
- One trace is sent to Datadog
28+
- `aws.lambda` span exists with correct properties including `cold_start: 'true'`
29+
- `aws.lambda.cold_start` span is created
30+
- `aws.lambda.load` spand is created for python and node.
31+
32+
**Build Requirements:**
33+
34+
For Java and .NET tests, Lambda functions must be built before deployment:
35+
36+
```bash
37+
# Build Java Lambda (uses Docker)
38+
cd lambda/base-java && ./build.sh
39+
40+
# Build .NET Lambda (uses Docker)
41+
cd lambda/base-dotnet && ./build.sh
42+
```
43+
44+
These builds use Docker to ensure cross-platform compatibility and do not require local Maven or .NET SDK installation.
1245

1346
## Guidelines
1447

integration-tests/bin/app.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,37 @@
11
#!/usr/bin/env node
22
import 'source-map-support/register';
33
import * as cdk from 'aws-cdk-lib';
4-
import { BaseNodeStack } from '../lib/stacks/base-node-stack';
5-
import { BasePythonStack } from '../lib/stacks/base-python-stack';
6-
import { getIdentifier } from '../tests/utils/config';
4+
import {BaseNodeStack} from '../lib/stacks/base-node-stack';
5+
import {BasePythonStack} from '../lib/stacks/base-python-stack';
6+
import {BaseJavaStack} from '../lib/stacks/base-java-stack';
7+
import {BaseDotnetStack} from '../lib/stacks/base-dotnet-stack';
8+
import {getIdentifier} from '../tests/utils/config';
79

810
const app = new cdk.App();
911

1012
const env = {
11-
account: process.env.CDK_DEFAULT_ACCOUNT || process.env.AWS_ACCOUNT_ID,
12-
region: process.env.CDK_DEFAULT_REGION || process.env.AWS_REGION || 'us-east-1',
13+
account: process.env.CDK_DEFAULT_ACCOUNT || process.env.AWS_ACCOUNT_ID,
14+
region: process.env.CDK_DEFAULT_REGION || process.env.AWS_REGION || 'us-east-1',
1315
};
1416

1517
const identifier = getIdentifier();
1618

17-
new BaseNodeStack(app, `integ-${identifier}-base-node`, {
18-
env,
19-
});
19+
const stacks = [
20+
new BaseNodeStack(app, `integ-${identifier}-base-node`, {
21+
env,
22+
}),
23+
new BasePythonStack(app, `integ-${identifier}-base-python`, {
24+
env,
25+
}),
26+
new BaseJavaStack(app, `integ-${identifier}-base-java`, {
27+
env,
28+
}),
29+
new BaseDotnetStack(app, `integ-${identifier}-base-dotnet`, {
30+
env,
31+
}),
32+
]
2033

21-
new BasePythonStack(app, `integ-${identifier}-base-python`, {
22-
env,
23-
});
34+
// Tag all stacks so we can easily clean them up
35+
stacks.forEach(stack => stack.addStackTag("extension_integration_test", "true"))
2436

2537
app.synth();
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
bin/
2+
obj/
3+
*.user
4+
*.suo
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Amazon.Lambda.Core;
2+
using System.Collections.Generic;
3+
4+
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
5+
6+
namespace Function
7+
{
8+
public class Handler
9+
{
10+
public Dictionary<string, object> FunctionHandler(Dictionary<string, object> input, ILambdaContext context)
11+
{
12+
context.Logger.LogLine("Hello world!");
13+
return new Dictionary<string, object>
14+
{
15+
{ "statusCode", 200 }
16+
};
17+
}
18+
}
19+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net8.0</TargetFramework>
4+
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
5+
<AWSProjectType>Lambda</AWSProjectType>
6+
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
7+
<PublishReadyToRun>true</PublishReadyToRun>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Amazon.Lambda.Core" Version="2.2.0" />
12+
<PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.4.0" />
13+
</ItemGroup>
14+
</Project>

0 commit comments

Comments
 (0)