Skip to content

Commit 232a5a1

Browse files
committed
feat(otel): Add checkpoint restart-recovery integration test
Verify file_storage checkpoint persistence prevents log loss across agent pod restarts. Uses the existing nginx-test deployment as a marker source — no additional terraform resources needed. Test flow: - Confirms agent is tailing nginx-test (warmup marker) - Emits 20 numbered markers via /proc/1/fd/1 - Kills agent pod mid-sequence (after marker 10) - Emits remaining markers while agent restarts - Queries CW Logs and asserts all 20 arrive exactly once Validates: no gaps (checkpoint resumes correctly) and no duplicates (offset is accurate).
1 parent 6937feb commit 232a5a1

1 file changed

Lines changed: 143 additions & 0 deletions

File tree

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
//go:build integration
2+
3+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4+
// SPDX-License-Identifier: MIT
5+
6+
package standard
7+
8+
import (
9+
"context"
10+
"fmt"
11+
"os/exec"
12+
"strings"
13+
"testing"
14+
"time"
15+
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
const (
20+
checkpointTargetDeploy = "deploy/nginx-test"
21+
checkpointTargetNS = "default"
22+
checkpointTotalMarkers = 20
23+
checkpointKillAfter = 10
24+
)
25+
26+
func TestCheckpointRestartRecovery(t *testing.T) {
27+
ctx := context.Background()
28+
runID := fmt.Sprintf("ckpt-%d", time.Now().UnixNano()%1000000)
29+
30+
podInfo, err := kubectlRun("get", "pod", "-n", checkpointTargetNS,
31+
"-l", "app=nginx-test",
32+
"--field-selector=status.phase=Running",
33+
"-o", "jsonpath={.items[0].metadata.name},{.items[0].spec.nodeName}")
34+
require.NoError(t, err, "finding nginx-test pod")
35+
parts := strings.SplitN(strings.TrimSpace(podInfo), ",", 2)
36+
require.Len(t, parts, 2, "expected pod,node from nginx-test")
37+
nginxPod, nginxNode := parts[0], parts[1]
38+
t.Logf("nginx-test pod: %s on node: %s", nginxPod, nginxNode)
39+
40+
agentPod, err := kubectlRun("get", "pod", "-n", "amazon-cloudwatch",
41+
"-l", "app.kubernetes.io/name=cloudwatch-agent",
42+
"--field-selector=spec.nodeName="+nginxNode+",status.phase=Running",
43+
"-o", "jsonpath={.items[0].metadata.name}")
44+
require.NoError(t, err, "finding agent pod")
45+
agentPod = strings.TrimSpace(agentPod)
46+
require.NotEmpty(t, agentPod, "no agent pod on node %s", nginxNode)
47+
t.Logf("agent pod: %s", agentPod)
48+
49+
// Step 1: Warmup — confirm agent is tailing nginx-test.
50+
warmup := fmt.Sprintf("%s-warmup", runID)
51+
emitMarkerToNginx(t, warmup)
52+
t.Log("warmup emitted, waiting 2 min")
53+
time.Sleep(2 * time.Minute)
54+
55+
count := queryMarkerCount(t, ctx, warmup, 5*time.Minute)
56+
require.True(t, count > 0, "warmup marker not in CW Logs — agent not tailing nginx-test")
57+
t.Log("warmup confirmed")
58+
59+
// Step 2: Emit first batch.
60+
t.Logf("emitting markers 1-%d", checkpointKillAfter)
61+
for i := 1; i <= checkpointKillAfter; i++ {
62+
emitMarkerToNginx(t, fmt.Sprintf("%s-marker-%03d", runID, i))
63+
time.Sleep(300 * time.Millisecond)
64+
}
65+
time.Sleep(5 * time.Second)
66+
67+
// Step 3: Kill agent.
68+
t.Logf("killing agent pod %s", agentPod)
69+
_, err = kubectlRun("delete", "pod", "-n", "amazon-cloudwatch", agentPod, "--grace-period=0", "--force")
70+
require.NoError(t, err)
71+
time.Sleep(3 * time.Second)
72+
73+
// Step 4: Emit remaining markers while agent is down.
74+
t.Logf("emitting markers %d-%d (agent restarting)", checkpointKillAfter+1, checkpointTotalMarkers)
75+
for i := checkpointKillAfter + 1; i <= checkpointTotalMarkers; i++ {
76+
emitMarkerToNginx(t, fmt.Sprintf("%s-marker-%03d", runID, i))
77+
time.Sleep(300 * time.Millisecond)
78+
}
79+
80+
// Step 5: Wait for restart.
81+
t.Log("waiting for agent restart")
82+
require.Eventually(t, func() bool {
83+
out, _ := kubectlRun("get", "pod", "-n", "amazon-cloudwatch",
84+
"-l", "app.kubernetes.io/name=cloudwatch-agent",
85+
"--field-selector=spec.nodeName="+nginxNode+",status.phase=Running",
86+
"-o", "jsonpath={.items[0].metadata.name}")
87+
name := strings.TrimSpace(out)
88+
return name != "" && name != agentPod
89+
}, 120*time.Second, 5*time.Second)
90+
91+
t.Log("waiting 4 min for propagation")
92+
time.Sleep(4 * time.Minute)
93+
94+
// Step 6: Verify all markers.
95+
seen := make(map[int]int)
96+
for i := 1; i <= checkpointTotalMarkers; i++ {
97+
seen[i] = queryMarkerCount(t, ctx, fmt.Sprintf("%s-marker-%03d", runID, i), 15*time.Minute)
98+
}
99+
100+
t.Log("results:")
101+
for i := 1; i <= checkpointTotalMarkers; i++ {
102+
t.Logf(" marker-%03d: %d", i, seen[i])
103+
}
104+
105+
var missing []int
106+
for i := 1; i <= checkpointTotalMarkers; i++ {
107+
if seen[i] == 0 {
108+
missing = append(missing, i)
109+
}
110+
}
111+
require.Empty(t, missing, "GAPS — markers not delivered: %v", missing)
112+
113+
var dupes []string
114+
for i := 1; i <= checkpointTotalMarkers; i++ {
115+
if seen[i] > 1 {
116+
dupes = append(dupes, fmt.Sprintf("%03d(x%d)", i, seen[i]))
117+
}
118+
}
119+
require.Empty(t, dupes, "DUPLICATES: %v", dupes)
120+
121+
t.Logf("PASS: all %d markers delivered exactly once", checkpointTotalMarkers)
122+
}
123+
124+
func emitMarkerToNginx(t *testing.T, msg string) {
125+
t.Helper()
126+
_, err := kubectlRun("exec", "-n", checkpointTargetNS, checkpointTargetDeploy, "--",
127+
"sh", "-c", fmt.Sprintf("echo '%s' >> /proc/1/fd/1", msg))
128+
require.NoError(t, err, "emitting %q", msg)
129+
}
130+
131+
func queryMarkerCount(t *testing.T, ctx context.Context, marker string, lookback time.Duration) int {
132+
t.Helper()
133+
query := fmt.Sprintf(`fields @message | filter @message like '%s' | limit 100`, marker)
134+
results, err := logsClient.QueryRaw(ctx, appLogGroup(), query, lookback)
135+
require.NoError(t, err, "querying %q", marker)
136+
return len(results)
137+
}
138+
139+
func kubectlRun(args ...string) (string, error) {
140+
cmd := exec.Command("kubectl", args...)
141+
out, err := cmd.CombinedOutput()
142+
return string(out), err
143+
}

0 commit comments

Comments
 (0)