Skip to content

Commit 7aeb121

Browse files
feat: Add RIE-based remote debugging infrastructure for bottlecap (#908)
https://datadoghq.atlassian.net/browse/SVLS-7833 Implements comprehensive remote debugging support for the bottlecap Lambda extension using Docker and GDB/LLDB, enabling developers to debug the extension in a local Lambda-like environment. **Core Changes:** 1. Build System Improvements: - Fix DEBUG flag not being passed to Docker build in compile_bottlecap.sh - Add DEBUG build arg validation and normalization (defaults to release mode) - Dockerfile now conditionally builds release vs debug based on DEBUG flag - Add explicit [profile.dev] configuration in Cargo.toml with debug symbols 2. Extension Debugging Support: - Add DD_DEBUG_WAIT_FOR_ATTACH environment variable to pause startup - Allows debugger attachment before extension initialization - Configurable wait period for comfortable debugger setup 3. Local Testing Infrastructure (local_tests/bottlecap/): - Custom Lambda Runtime Interface Emulator (RIE) binaries for arm64/x86 - Dockerfile with debug tools (gdb, gdbserver, lldb, procps) - Extended Lambda timeout to 900s (15 min) for debugging sessions - Simple Python test handler for triggering invocations 4. Developer Workflow Automation: - Makefile with targets: build, start, attach, invoke, logs, status, shell, stop - Support for both post-startup and startup debugging workflows - Port mappings: 9000 (Lambda) and 2345 (GDB remote) 5. Documentation: - DEBUG_GUIDE.md - README.md - Examples for RustRover IDE configuration **Technical Details:** - Fixes critical bug where DEBUG=0 was set in shell but never passed to docker buildx - Dockerfile now correctly uses ${BUILD_MODE} to select target/release or target/debug - Debug builds include full symbols (debuginfo=2, strip=false, opt-level=0) - Container runs with --cap-add=SYS_PTRACE for gdbserver attachment - Path mapping: /tmp/dd/bottlecap → local bottlecap/ for source debugging **Usage:** ```bash # Build with debug symbols and start debugging make build make start make invoke # Start extension (runs on-demand) make attach # Attach gdbserver # Connect RustRover to localhost:2345 # Or debug from initialization make start DEBUG_WAIT=15 make invoke & make attach # Within 15 seconds This enables efficient local debugging of Lambda extension logic including initialization, event processing, telemetry handling, and flushing behavior.
1 parent 7fd8db7 commit 7aeb121

13 files changed

Lines changed: 866 additions & 3 deletions

File tree

.gitlab/scripts/compile_bottlecap.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ else
3131
printf "Fips compile requested: ${FIPS}\n"
3232
fi
3333

34+
if [ -z "$DEBUG" ] || [ "$DEBUG" != "1" ]; then
35+
printf "[WARNING] No DEBUG provided or DEBUG != 1, defaulting to release mode\n"
36+
DEBUG=0
37+
else
38+
printf "Debug mode requested: ${DEBUG}\n"
39+
fi
40+
3441
if [ "$ALPINE" = "0" ]; then
3542
COMPILE_IMAGE=Dockerfile.bottlecap.compile
3643
else
@@ -67,6 +74,7 @@ docker_build() {
6774
-f ./images/${file} \
6875
--build-arg PLATFORM=$PLATFORM \
6976
--build-arg FIPS="${FIPS}" \
77+
--build-arg DEBUG="${DEBUG}" \
7078
. -o $BINARY_PATH
7179

7280
# Copy the compiled binary to the target directory with the expected name

bottlecap/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ tempfile = "3.20"
8181
[[bin]]
8282
name = "bottlecap"
8383

84+
[profile.dev]
85+
debug = true # same as debuginfo=2 and no stripping
86+
strip = false
87+
opt-level = 0
88+
8489
[profile.release]
8590
opt-level = "z" # Optimize for size.
8691
lto = true

bottlecap/src/bin/bottlecap/main.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,17 @@ async fn main() -> anyhow::Result<()> {
256256
log_fips_status(&aws_config.region);
257257
let version_without_next = EXTENSION_VERSION.split('-').next().unwrap_or("NA");
258258
debug!("Starting Datadog Extension v{version_without_next}");
259+
260+
// Debug: Wait for debugger to attach if DD_DEBUG_WAIT_FOR_ATTACH is set
261+
if let Ok(wait_secs) = env::var("DD_DEBUG_WAIT_FOR_ATTACH") {
262+
if let Ok(secs) = wait_secs.parse::<u64>() {
263+
debug!("DD_DEBUG_WAIT_FOR_ATTACH: Waiting {secs} seconds for debugger to attach...");
264+
debug!("Connect your debugger to port 2345 now!");
265+
tokio::time::sleep(tokio::time::Duration::from_secs(secs)).await;
266+
debug!("DD_DEBUG_WAIT_FOR_ATTACH: Continuing execution...");
267+
}
268+
}
269+
259270
prepare_client_provider()?;
260271
let client = create_reqwest_client_builder()
261272
.map_err(|e| anyhow::anyhow!("Failed to create client builder: {e:?}"))?

images/Dockerfile.bottlecap.compile

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ FROM public.ecr.aws/lambda/provided:al2 AS compiler
22
ARG PLATFORM
33

44
ARG FIPS
5+
ARG DEBUG
56

67
# Install dependencies
78
RUN --mount=type=cache,target=/var/cache/yum \
@@ -35,11 +36,18 @@ RUN --mount=type=cache,target=/usr/local/cargo/git \
3536
else \
3637
export FEATURES=default; \
3738
fi; \
39+
if [ "${DEBUG}" = "0" ]; then \
40+
export BUILD_MODE=release; \
41+
export BUILD_FLAG="--release"; \
42+
else \
43+
export BUILD_MODE=debug; \
44+
export BUILD_FLAG=""; \
45+
fi; \
3846
# The `libddwaf` crate links against static objects that require `libclang_rt.builtins`, but
3947
# this is not presented to the linker by default on this platform, so we force it in.
4048
export RUSTFLAGS="${RUSTFLAGS:-} -Clinker=clang -L$(dirname $(clang --print-file-name="libclang_rt.builtins-$(uname -m).a")) -lclang_rt.builtins-$(uname -m)"; \
41-
cargo +stable build --verbose --locked --no-default-features --features="${FEATURES}" --release && \
42-
mkdir -p /tmp/out && cp "/tmp/dd/bottlecap/target/release/bottlecap" /tmp/out/bottlecap
49+
cargo +stable build --verbose --locked --no-default-features --features="${FEATURES}" ${BUILD_FLAG} && \
50+
mkdir -p /tmp/out && cp "/tmp/dd/bottlecap/target/${BUILD_MODE}/bottlecap" /tmp/out/bottlecap
4351

4452
# Use smallest image possible
4553
FROM scratch
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
# Debugging Bottlecap Extension Guide
2+
3+
This guide explains how to debug the bottlecap Lambda extension running in a local Docker container using RustRover (or any GDB-compatible debugger).
4+
5+
## Overview
6+
7+
The bottlecap extension is a Rust-based AWS Lambda extension. Unlike traditional processes that start when the container starts, Lambda extensions only initialize when Lambda is invoked for the first time.
8+
9+
**Key Insight**: You must trigger at least one Lambda invocation (`make invoke`) before the extension process exists and can be debugged. The debugging sequence is: build → start → **invoke** → attach → debug.
10+
11+
This guide covers two debugging approaches:
12+
13+
1. **Post-Startup Debugging**: Attach after the extension has started (recommended for most cases)
14+
2. **Startup Debugging**: Debug from extension initialization using a wait period
15+
16+
## Prerequisites
17+
18+
- Docker running on your machine
19+
- RustRover or CLion IDE (or any GDB-compatible debugger)
20+
- The bottlecap extension built with debug symbols (`DEBUG=1`)
21+
- RIE binary: The setup uses a custom Lambda Runtime Interface Emulator.
22+
23+
## Understanding Lambda Extension Lifecycle
24+
25+
Lambda extensions are **on-demand processes** that behave differently from traditional Docker processes:
26+
27+
1. **Container starts** (`make start`): The Lambda RIE starts, but the extension does NOT run yet
28+
2. **First invocation** (`make invoke`): Lambda initializes and starts the extension process
29+
3. **Extension registers**: The extension registers with Lambda Runtime API and enters its event loop
30+
4. **Ready for debugging**: Only now does the datadog-agent process exist and can be attached
31+
32+
This means:
33+
- Running `docker ps` shows the container, but the extension process isn't visible yet
34+
- `make attach` will fail if no invocation has occurred
35+
- You must always invoke Lambda first to start the extension before debugging
36+
37+
## Method 1: Post-Startup Debugging (Recommended)
38+
39+
This approach lets you attach the debugger after the extension has started and is ready to process invocations.
40+
41+
### Step 1: Build and Start the Container
42+
43+
```bash
44+
cd local_tests/bottlecap
45+
46+
# Build the extension with debug symbols
47+
make build
48+
49+
# Start the container
50+
make start
51+
```
52+
53+
### Step 2: Trigger First Invocation
54+
55+
The extension only starts when Lambda is invoked for the first time:
56+
57+
```bash
58+
# In one terminal, monitor logs
59+
make logs
60+
61+
# In another terminal, trigger invocation
62+
make invoke
63+
```
64+
65+
Watch the logs for:
66+
```
67+
DD_EXTENSION | DEBUG | Starting Datadog Extension v88
68+
DD_EXTENSION | DEBUG | Datadog Next-Gen Extension ready in 49ms
69+
```
70+
71+
### Step 3: Attach the Debugger
72+
73+
Once the extension is running, attach gdbserver:
74+
75+
```bash
76+
make attach
77+
```
78+
79+
You should see:
80+
```
81+
Attached; pid = 12
82+
Listening on port 2345
83+
```
84+
85+
### Step 4: Connect RustRover
86+
87+
1. In RustRover, go to **Run → Edit Configurations**
88+
2. Click **+** and select **Remote Debug**
89+
3. Configure:
90+
- **Name**: `Bottlecap Remote Debug`
91+
- **Target**: `localhost:2345`
92+
- **Symbol file**: `<PROJECT_ROOT>/.binaries/bottlecap-arm64`
93+
- Leave **Sysroot** empty
94+
4. Click **OK**
95+
96+
5. Set breakpoints in your code (e.g., in `main.rs` or event handlers)
97+
98+
6. Start debugging: **Run → Debug 'Bottlecap Remote Debug'**
99+
100+
### Step 5: Trigger More Invocations
101+
102+
With the debugger attached and breakpoints set, trigger Lambda invocations to hit your breakpoints:
103+
104+
```bash
105+
# Trigger invocation
106+
make invoke
107+
108+
# Or trigger multiple times
109+
for i in {1..5}; do make invoke; sleep 1; done
110+
```
111+
112+
## Method 2: Startup Debugging
113+
114+
This approach is useful when you need to debug the extension initialization code itself.
115+
116+
### Step 1: Start with Debug Wait
117+
118+
```bash
119+
cd local_tests/bottlecap
120+
121+
# Build
122+
make build
123+
124+
# Start with 15-second wait for debugger
125+
make start DEBUG_WAIT=15
126+
```
127+
128+
This will display:
129+
```
130+
Container 'bottlecap-lambda' started. Use 'make logs' to view logs.
131+
Debug mode: Extension will wait 15 seconds for debugger on first invocation
132+
```
133+
134+
### Step 2: Trigger Invocation to Start Extension
135+
136+
```bash
137+
# In one terminal, watch logs
138+
make logs
139+
140+
# In another terminal, trigger the first invocation
141+
# This starts the extension which will wait for 15 seconds
142+
make invoke &
143+
```
144+
145+
Watch the logs for:
146+
```
147+
DD_EXTENSION | DEBUG | Starting Datadog Extension v88
148+
DD_EXTENSION | DEBUG | DD_DEBUG_WAIT_FOR_ATTACH: Waiting 15 seconds for debugger to attach...
149+
DD_EXTENSION | DEBUG | Connect your debugger to port 2345 now!
150+
```
151+
152+
### Step 3: Quickly Attach and Connect
153+
154+
You have 15 seconds to attach gdbserver and connect your debugger:
155+
156+
```bash
157+
# Attach gdbserver (in a new terminal)
158+
make attach
159+
```
160+
161+
Then immediately connect RustRover as described in Method 1, Step 4.
162+
163+
### Step 4: Debug Initialization
164+
165+
The extension is now paused during initialization. You can:
166+
- Step through the initialization code
167+
- Set breakpoints in event handlers
168+
- Continue execution to let the extension finish starting
169+
170+
After the wait period expires, you'll see:
171+
```
172+
DD_EXTENSION | DEBUG | DD_DEBUG_WAIT_FOR_ATTACH: Continuing execution...
173+
```
174+
175+
## Common Debugging Scenarios
176+
177+
### Debug a Specific Invocation Handler
178+
179+
1. Start container: `make start`
180+
2. Trigger first invocation to start extension: `make invoke`
181+
3. Attach debugger: `make attach`
182+
4. Set breakpoint in the handler (e.g., `main.rs:1143` in `handle_next_invocation`)
183+
5. Connect RustRover debugger
184+
6. Trigger another invocation: `make invoke`
185+
186+
### Debug Telemetry Event Processing
187+
188+
1. Start container: `make start DEBUG_WAIT=15`
189+
2. Trigger invocation to start extension: `make invoke &`
190+
3. Quickly attach: `make attach`
191+
4. Set breakpoints in `handle_event_bus_event` (main.rs:1020)
192+
5. Connect debugger
193+
6. Let execution continue - breakpoints will hit as telemetry events arrive
194+
195+
### Debug Metrics/Logs/Traces Flushing
196+
197+
1. Start container normally: `make start`
198+
2. Trigger invocation: `make invoke`
199+
3. Attach: `make attach`
200+
4. Set breakpoints in flushing code (main.rs:925 `blocking_flush_all`)
201+
5. Connect debugger
202+
6. Trigger more invocations to hit flush logic
203+
204+
## Troubleshooting
205+
206+
### "the input device is not a TTY" Error
207+
208+
This happens when running `make attach` from a non-interactive shell. The command requires TTY for gdbserver interaction. Run from a normal terminal session.
209+
210+
### Extension Process Not Found
211+
212+
If `make attach` can't find the datadog-agent process:
213+
- Ensure you triggered at least one invocation: `make invoke`
214+
- Check logs: `make logs` - look for "Starting Datadog Extension"
215+
- Verify container is running: `make status`
216+
217+
### Debugger Can't Connect to Port 2345
218+
219+
- Check the container exposes port 2345: `docker ps` should show `0.0.0.0:2345->2345/tcp`
220+
- Verify gdbserver is listening: `make attach` should show "Listening on port 2345"
221+
- Check firewall settings
222+
223+
### Extension Exits Immediately
224+
225+
Lambda RIE may be shutting down the environment. Check:
226+
- Container logs: `make logs`
227+
- Ensure you're using the correct RIE binary
228+
- The container must use the RIE from `local_tests/rie/bottlecap/arm64/rie`
229+
230+
### Timeout During Startup Debug Wait
231+
232+
If 15 seconds isn't enough:
233+
- Increase the wait time: `make start DEBUG_WAIT=30`
234+
- Or skip startup debugging and use post-startup method
235+
236+
### Lambda Invocation Times Out After 1 Second
237+
238+
If you see "Task timed out after 1.00 seconds" when running `make invoke`:
239+
- **Root cause**: The `AWS_LAMBDA_FUNCTION_TIMEOUT` environment variable in the Dockerfile was set to 1 second
240+
- **Fix**: The Dockerfile has been updated to use 900 seconds (15 minutes) to allow comfortable debugging
241+
- **Verify**: Check `Dockerfile` line 7 shows `AWS_LAMBDA_FUNCTION_TIMEOUT=900`
242+
- **Why 15 minutes**: This is AWS Lambda's maximum timeout, giving you plenty of time to step through code, inspect variables, and debug complex logic without the invocation timing out
243+
244+
## Key Code Locations for Breakpoints
245+
246+
- **Extension main entry**: `bottlecap/src/bin/bottlecap/main.rs:251` (`main()`)
247+
- **Extension registration**: `main.rs:270` (`extension::register`)
248+
- **Event loop (OnDemand mode)**: `main.rs:697` (OnDemand loop)
249+
- **Invocation handling**: `main.rs:1143` (`handle_next_invocation`)
250+
- **Event processing**: `main.rs:1020` (`handle_event_bus_event`)
251+
- **Flushing**: `main.rs:925` (`blocking_flush_all`)
252+
253+
## Additional Resources
254+
255+
- [GDB Remote Debugging](https://sourceware.org/gdb/current/onlinedocs/gdb/Remote-Debugging.html)
256+
- [RustRover Remote Debug Guide](https://www.jetbrains.com/help/rust/remote-debug.html)
257+
- [AWS Lambda Extensions API](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-extensions-api.html)
258+
259+
## Clean Up
260+
261+
When done debugging:
262+
263+
```bash
264+
# Stop the container
265+
make stop
266+
267+
# View help for other commands
268+
make help
269+
```

local_tests/bottlecap/Dockerfile

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
FROM public.ecr.aws/lambda/python:3.9
2+
3+
# Build argument for parent directory name
4+
ARG TARGET=bottlecap
5+
6+
# Set to 900 (15 minutes) for debugging - Lambda max timeout
7+
ENV AWS_LAMBDA_FUNCTION_TIMEOUT=900
8+
ENV LOG_LEVEL=debug
9+
ENV DD_LOG_LEVEL=debug
10+
# Enforce flushing
11+
ENV DD_FLUSHING_STRATEGY=continuously,1
12+
13+
14+
# ---- Add remote dev tooling ----
15+
# SSH server, LLDB, GDB, procps (ps/top), curl, git
16+
RUN yum -y install lldb gdb gdb-gdbserver procps-ng tar gzip zip curl git shadow-utils sysvinit-tools\
17+
&& \
18+
yum -y install \
19+
fontconfig \
20+
freetype \
21+
libXrender \
22+
libXext \
23+
libXi \
24+
libXtst \
25+
libX11 \
26+
alsa-lib \
27+
which \
28+
&& yum clean all
29+
30+
# Optional: install Rust toolchain (useful if you want to build inside the container)
31+
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
32+
ENV PATH=/root/.cargo/bin:$PATH
33+
34+
35+
# The first path is where the custom RIE lives.
36+
COPY local_tests/rie/${TARGET}/arm64/rie /usr/local/bin/aws-lambda-rie
37+
38+
# The first path is the location of your local agent binary
39+
COPY .binaries/bottlecap-arm64 /opt/extensions/datadog-agent
40+
41+
# Copy the lambda function to the container
42+
COPY local_tests/${TARGET}/index.py ${LAMBDA_TASK_ROOT}/
43+
44+
CMD ["index.handler"]
45+
46+
ENTRYPOINT ["/entrypoint.sh"]

0 commit comments

Comments
 (0)