Skip to content

Commit 417d354

Browse files
committed
feat(go-forwarder): add tests
1 parent 8568a56 commit 417d354

6 files changed

Lines changed: 302 additions & 45 deletions

File tree

aws/logs_monitoring_go/go.mod

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,27 @@ module github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go
22

33
go 1.26
44

5-
require github.com/aws/aws-lambda-go v1.53.0
5+
require (
6+
github.com/aws/aws-lambda-go v1.53.0
7+
github.com/aws/aws-sdk-go-v2 v1.41.4
8+
github.com/aws/aws-sdk-go-v2/config v1.32.12
9+
github.com/stretchr/testify v1.11.1
10+
)
611

712
require (
8-
github.com/aws/aws-sdk-go-v2 v1.41.4 // indirect
9-
github.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect
1013
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect
1114
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect
1215
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect
1316
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect
1417
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
1518
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
1619
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect
17-
github.com/aws/aws-sdk-go-v2/service/kms v1.50.3 // indirect
18-
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.4 // indirect
1920
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect
20-
github.com/aws/aws-sdk-go-v2/service/ssm v1.68.3 // indirect
2121
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect
2222
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
2323
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect
2424
github.com/aws/smithy-go v1.24.2 // indirect
25+
github.com/davecgh/go-spew v1.1.1 // indirect
26+
github.com/pmezard/go-difflib v1.0.0 // indirect
27+
gopkg.in/yaml.v3 v3.0.1 // indirect
2528
)

aws/logs_monitoring_go/go.sum

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhL
1818
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
1919
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y=
2020
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=
21-
github.com/aws/aws-sdk-go-v2/service/kms v1.50.3 h1:s/zDSG/a/Su9aX+v0Ld9cimUCdkr5FWPmBV8owaEbZY=
22-
github.com/aws/aws-sdk-go-v2/service/kms v1.50.3/go.mod h1:/iSgiUor15ZuxFGQSTf3lA2FmKxFsQoc2tADOarQBSw=
23-
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.4 h1:9aZbO86sraeCIHHCpZhxwN9tnVy9POkSKzi4/TpT54A=
24-
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.4/go.mod h1:cxiXDhEzIq7Xx1BtmC4lGBK3SwAZ79+EUWiKawYHo14=
2521
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=
2622
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=
27-
github.com/aws/aws-sdk-go-v2/service/ssm v1.68.3 h1:bBoWhx8lsFLTXintRX64ZBXcmFZbGqUmaPUrjXECqIc=
28-
github.com/aws/aws-sdk-go-v2/service/ssm v1.68.3/go.mod h1:rcRkKbUJ2437WuXdq9fbj+MjTudYWzY9Ct8kiBbN8a8=
2923
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=
3024
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=
3125
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=
@@ -38,7 +32,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
3832
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3933
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4034
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
41-
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
42-
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
35+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
36+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
37+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
38+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
4339
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
4440
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

aws/logs_monitoring_go/internal/config/apikey.go

Lines changed: 82 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ package config
77

88
import (
99
"context"
10+
"crypto/tls"
1011
"errors"
1112
"fmt"
1213
"log/slog"
14+
"net/http"
1315
"os"
1416
"time"
1517

@@ -19,7 +21,9 @@ import (
1921
)
2022

2123
const (
22-
awsClientTimeout = 5 * time.Second
24+
httpClientTimeout = 10 * time.Second
25+
maxRetries = 5
26+
retryBackoffFactor = 1 * time.Second
2327
)
2428

2529
type resolveOptions struct {
@@ -29,6 +33,14 @@ type resolveOptions struct {
2933

3034
type apiKeyResolver func(ctx context.Context, opts resolveOptions) (string, error)
3135

36+
var retryableStatusCodes = map[int]bool{
37+
429: true,
38+
500: true,
39+
502: true,
40+
503: true,
41+
504: true,
42+
}
43+
3244
var resolvers = []struct {
3345
envVar string
3446
resolve apiKeyResolver
@@ -41,7 +53,7 @@ var resolvers = []struct {
4153

4254
func (c *Config) resolveAPIKey(ctx context.Context) error {
4355
awsCfg, err := awsconfig.LoadDefaultConfig(ctx,
44-
awsconfig.WithHTTPClient(awshttp.NewBuildableClient().WithTimeout(awsClientTimeout)),
56+
awsconfig.WithHTTPClient(awshttp.NewBuildableClient().WithTimeout(httpClientTimeout)),
4557
)
4658

4759
if err != nil {
@@ -65,6 +77,74 @@ func (c *Config) resolveAPIKey(ctx context.Context) error {
6577
return errors.New("no API key configured: set DD_API_KEY_SECRET_ARN, DD_API_KEY_SSM_NAME, DD_KMS_API_KEY, or DD_API_KEY. See: https://docs.datadoghq.com/serverless/forwarder/")
6678
}
6779

80+
// Note: the API key verification could fail (e.g. Datadog verification endpoint or network problem)
81+
// Instead of failing the whole lambda at startup, it should run up to the log sending part and verify the
82+
// key at this moment, adding the run to the future retry logic in such case.
83+
// The method may disappear in the future.
84+
func (c *Config) validateAPIKey() error {
85+
if c.APIKey == "" || c.APIKey == "<YOUR_DATADOG_API_KEY>" {
86+
return errors.New("missing Datadog API key. Set DD_API_KEY environment variable. See: https://docs.datadoghq.com/serverless/forwarder/")
87+
}
88+
89+
if len(c.APIKey) != 32 {
90+
return fmt.Errorf("invalid Datadog API key format: expected 32 characters, got %d. Verify your API key at https://app.%s/organization-settings/api-keys", len(c.APIKey), c.Site)
91+
}
92+
93+
slog.Debug("validating Datadog API key")
94+
95+
client := &http.Client{
96+
Timeout: httpClientTimeout,
97+
Transport: &http.Transport{
98+
TLSClientConfig: &tls.Config{
99+
InsecureSkipVerify: c.SkipSSLValidation,
100+
},
101+
},
102+
}
103+
104+
url := fmt.Sprintf("%s/api/v1/validate?api_key=%s", c.APIURL, c.APIKey)
105+
106+
var lastErr error
107+
var lastStatus int
108+
for attempt := range maxRetries {
109+
resp, err := client.Get(url)
110+
if err != nil {
111+
lastErr = err
112+
slog.Debug("API key validation request failed, retrying", "attempt", attempt+1, "error", err)
113+
time.Sleep(retryBackoffFactor * time.Duration(1<<attempt))
114+
continue
115+
}
116+
resp.Body.Close()
117+
118+
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
119+
return nil
120+
}
121+
122+
if !retryableStatusCodes[resp.StatusCode] {
123+
slog.Warn("API key validation failed. Verify your API key is correct and DD_SITE matches your Datadog account region. See: https://docs.datadoghq.com/getting_started/site/",
124+
"status", resp.StatusCode,
125+
"site", c.Site,
126+
)
127+
return nil
128+
}
129+
130+
lastStatus = resp.StatusCode
131+
slog.Debug("API key validation returned retryable status, retrying", "attempt", attempt+1, "status", resp.StatusCode)
132+
time.Sleep(retryBackoffFactor * time.Duration(1<<attempt))
133+
}
134+
135+
if lastErr != nil {
136+
slog.Warn("API key validation failed after retries, continuing anyway", "attempts", maxRetries, "error", lastErr)
137+
} else {
138+
slog.Warn("API key validation failed after retries. Verify your API key is correct and DD_SITE matches your Datadog account region. See: https://docs.datadoghq.com/getting_started/site/",
139+
"attempts", maxRetries,
140+
"lastStatus", lastStatus,
141+
"site", c.Site,
142+
)
143+
}
144+
145+
return nil
146+
}
147+
68148
func resolveFromSecretsManager(ctx context.Context, opts resolveOptions) (string, error) {
69149
return "", nil
70150
}
@@ -78,32 +158,5 @@ func resolveFromKMS(ctx context.Context, opts resolveOptions) (string, error) {
78158
}
79159

80160
func resolveFromEnv(ctx context.Context, opts resolveOptions) (string, error) {
81-
// if len(opts.Value) != 32 {
82-
// return "", fmt.Errorf("invalid datadog api key format")
83-
// }
84-
85-
// client := &http.Client{
86-
// Timeout: TIMEOUT * time.Second,
87-
// Transport: &http.Transport{
88-
// TLSClientConfig: &tls.Config{
89-
// InsecureSkipVerify: skipSSLValidation,
90-
// },
91-
// },
92-
// }
93-
94-
// res, err := http.Get()
95-
// if err != nil {
96-
97-
// }
98-
99161
return opts.Value, nil
100162
}
101-
102-
// Note: the API key verification could fail (e.g. Datadog verification endpoint or network problem)
103-
// Instead of failing the whole lambda at startup, it should run up to the log sending part and verify the
104-
// key at this moment, adding the run to the future retry logic in such case.
105-
// The method may disappear in the future.
106-
func (c *Config) validateAPIKey() error {
107-
// TODO: implement validation against Datadog API
108-
return nil
109-
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package config
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestResolveAPIKey(t *testing.T) {
13+
t.Run("no env vars set", func(t *testing.T) {
14+
cfg := &Config{}
15+
err := cfg.resolveAPIKey(context.Background())
16+
assert.Error(t, err)
17+
assert.Contains(t, err.Error(), "no API key configured")
18+
})
19+
20+
t.Run("DD_API_KEY set", func(t *testing.T) {
21+
t.Setenv("DD_API_KEY", "abcdef1234567890abcdef1234567890")
22+
23+
cfg := &Config{}
24+
err := cfg.resolveAPIKey(context.Background())
25+
assert.NoError(t, err)
26+
assert.Equal(t, "abcdef1234567890abcdef1234567890", cfg.APIKey)
27+
})
28+
29+
t.Run("DD_API_KEY_SECRET_ARN takes priority over DD_API_KEY", func(t *testing.T) {
30+
t.Setenv("DD_API_KEY", "abcdef1234567890abcdef1234567890")
31+
t.Setenv("DD_API_KEY_SECRET_ARN", "arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret")
32+
33+
cfg := &Config{}
34+
err := cfg.resolveAPIKey(context.Background())
35+
assert.NoError(t, err)
36+
assert.Equal(t, "", cfg.APIKey, "should resolve from Secrets Manager, not DD_API_KEY")
37+
})
38+
}
39+
40+
func TestValidateAPIKey(t *testing.T) {
41+
t.Parallel()
42+
43+
t.Run("empty API key", func(t *testing.T) {
44+
t.Parallel()
45+
46+
cfg := &Config{APIKey: ""}
47+
err := cfg.validateAPIKey()
48+
assert.Error(t, err)
49+
assert.Contains(t, err.Error(), "missing Datadog API key")
50+
})
51+
52+
t.Run("wrong length", func(t *testing.T) {
53+
t.Parallel()
54+
55+
cfg := &Config{APIKey: "tooshort", Site: "datadoghq.com"}
56+
err := cfg.validateAPIKey()
57+
assert.Error(t, err)
58+
assert.Contains(t, err.Error(), "expected 32 characters, got 8")
59+
})
60+
61+
t.Run("valid key with successful validation", func(t *testing.T) {
62+
t.Parallel()
63+
64+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
65+
assert.Equal(t, "/api/v1/validate", r.URL.Path)
66+
assert.Equal(t, "abcdef1234567890abcdef1234567890", r.URL.Query().Get("api_key"))
67+
w.WriteHeader(http.StatusOK)
68+
}))
69+
defer server.Close()
70+
71+
cfg := &Config{
72+
APIKey: "abcdef1234567890abcdef1234567890",
73+
APIURL: server.URL,
74+
Site: "datadoghq.com",
75+
}
76+
err := cfg.validateAPIKey()
77+
assert.NoError(t, err)
78+
})
79+
80+
t.Run("valid key with 403 response", func(t *testing.T) {
81+
t.Parallel()
82+
83+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
84+
w.WriteHeader(http.StatusForbidden)
85+
}))
86+
defer server.Close()
87+
88+
cfg := &Config{
89+
APIKey: "abcdef1234567890abcdef1234567890",
90+
APIURL: server.URL,
91+
Site: "datadoghq.com",
92+
}
93+
err := cfg.validateAPIKey()
94+
assert.NoError(t, err)
95+
})
96+
97+
t.Run("retryable 500 then recovers with 200", func(t *testing.T) {
98+
t.Parallel()
99+
100+
callCount := 0
101+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
102+
callCount++
103+
if callCount <= 1 {
104+
w.WriteHeader(http.StatusInternalServerError)
105+
return
106+
}
107+
w.WriteHeader(http.StatusOK)
108+
}))
109+
defer server.Close()
110+
111+
cfg := &Config{
112+
APIKey: "abcdef1234567890abcdef1234567890",
113+
APIURL: server.URL,
114+
Site: "datadoghq.com",
115+
}
116+
err := cfg.validateAPIKey()
117+
assert.NoError(t, err)
118+
assert.Equal(t, 2, callCount, "should have retried once then succeeded")
119+
})
120+
}

aws/logs_monitoring_go/internal/config/config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ var deprecatedEnvironmentVariables = []string{
3030
type Config struct {
3131
APIKey string
3232
Site string
33-
URL string
33+
IntakeURL string
3434
Port int
3535
APIURL string
3636
NoSSL bool
@@ -69,7 +69,7 @@ func (c *Config) deriveURLs() {
6969
if c.NoSSL {
7070
scheme = "http"
7171
}
72-
c.URL = envOrDefault("DD_URL", "http-intake.logs."+c.Site)
72+
c.IntakeURL = envOrDefault("DD_URL", "http-intake.logs."+c.Site)
7373
c.APIURL = envOrDefault("DD_API_URL", fmt.Sprintf("%s://api.%s", scheme, c.Site))
7474
}
7575

0 commit comments

Comments
 (0)