Skip to content

Commit 507a75b

Browse files
authored
refactor(go-forwarder): decouple key resolution, cache clients and share AWS config (#1151)
1 parent df53d6f commit 507a75b

17 files changed

Lines changed: 341 additions & 247 deletions

File tree

aws/logs_monitoring_go/cmd/forwarder/main.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import (
1111
"errors"
1212
"fmt"
1313
"log"
14+
"log/slog"
1415

16+
"github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go/internal/apikey"
1517
"github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go/internal/config"
1618
"github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go/internal/filtering"
1719
"github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go/internal/forwarding"
@@ -37,16 +39,11 @@ func main() {
3739
}
3840
httpclient.Init(tlsOpts...)
3941

40-
// Will refactor this in the future to not stop the forwarder if the api key resolution or validation fails.
41-
// We may want to store the events in the storage retry mechanism even in case of API key resolution/expiration
42-
// so we let the customer some time to configure it properly and not lose any of the events from then.
43-
err = cfg.ResolveAPIKey(context.Background())
44-
if err != nil {
45-
log.Fatal(err)
46-
}
47-
err = cfg.ValidateAPIKey(context.Background())
48-
if err != nil {
49-
log.Fatal(err)
42+
if err = apikey.Validate(context.Background(), httpclient.Client, cfg.APIURL, cfg.APIKey); err != nil {
43+
if !cfg.StoreOnFail {
44+
log.Fatal(fmt.Errorf("no failed event storage set, validate API key: %w", err))
45+
}
46+
slog.Error("API key validation", slog.Any("error", err))
5047
}
5148

5249
lambda.Start(handleRequest(cfg))
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2026-Present Datadog, Inc.
5+
6+
package apikey
7+
8+
import (
9+
"context"
10+
"fmt"
11+
"io"
12+
"net/http"
13+
"time"
14+
15+
"github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go/internal/httpclient"
16+
)
17+
18+
const (
19+
apiKeyHeader = "DD-API-KEY"
20+
validationTimeout = 3 * time.Second
21+
)
22+
23+
func Validate(ctx context.Context, client *http.Client, url, key string) error {
24+
if !validFormat(key) {
25+
return invalidAPIKeyError{"bad format"}
26+
}
27+
28+
ctx, cancel := context.WithTimeout(ctx, validationTimeout)
29+
defer cancel()
30+
31+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
32+
if err != nil {
33+
return fmt.Errorf("new request: %w", err)
34+
}
35+
36+
req.Header.Set(apiKeyHeader, key)
37+
38+
resp, err := client.Do(req)
39+
if err != nil {
40+
return fmt.Errorf("client do: %w", err)
41+
}
42+
defer httpclient.DrainClose(resp)
43+
44+
if resp.StatusCode == http.StatusForbidden {
45+
return invalidAPIKeyError{"denied from validation endpoint"}
46+
}
47+
48+
if resp.StatusCode != http.StatusOK {
49+
if body, err := io.ReadAll(resp.Body); err == nil {
50+
return fmt.Errorf("unexpected HTTP/%d response: %s", resp.StatusCode, body)
51+
}
52+
return fmt.Errorf("unexpected HTTP/%d response", resp.StatusCode)
53+
}
54+
55+
return nil
56+
}
57+
58+
func validFormat(key string) bool {
59+
return len(key) == 32
60+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2026-Present Datadog, Inc.
5+
6+
package apikey
7+
8+
import (
9+
"net/http"
10+
"net/http/httptest"
11+
"testing"
12+
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestValidate(t *testing.T) {
18+
t.Parallel()
19+
20+
tests := map[string]struct {
21+
key string
22+
statusCode int
23+
wantErr bool
24+
err error
25+
}{
26+
"valid": {
27+
key: "0123456789abcdefghij0123456789ab",
28+
statusCode: http.StatusOK,
29+
},
30+
"wrong format": {
31+
key: "not32characters",
32+
wantErr: true,
33+
err: &invalidAPIKeyError{},
34+
},
35+
"invalid": {
36+
key: "myapikeyisexpiredorinvalid012345",
37+
statusCode: http.StatusForbidden,
38+
wantErr: true,
39+
err: &invalidAPIKeyError{},
40+
},
41+
"unexpected error": {
42+
key: "0123456789abcdefghij0123456789ab",
43+
statusCode: http.StatusInternalServerError,
44+
wantErr: true,
45+
},
46+
}
47+
48+
for name, tc := range tests {
49+
t.Run(name, func(t *testing.T) {
50+
t.Parallel()
51+
52+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
53+
assert.Equal(t, tc.key, req.Header.Get("DD-API-KEY"))
54+
w.WriteHeader(tc.statusCode)
55+
}))
56+
t.Cleanup(server.Close)
57+
58+
err := Validate(t.Context(), server.Client(), server.URL, tc.key)
59+
60+
if tc.wantErr {
61+
if tc.err != nil {
62+
require.ErrorAs(t, err, tc.err)
63+
}
64+
require.Error(t, err)
65+
return
66+
}
67+
68+
require.NoError(t, err)
69+
})
70+
}
71+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2026-Present Datadog, Inc.
5+
6+
package apikey
7+
8+
type invalidAPIKeyError struct {
9+
message string
10+
}
11+
12+
func (e invalidAPIKeyError) Error() string {
13+
return "invalid Datadog API key: " + e.message + ". See https://docs.datadoghq.com/serverless/forwarder/ for more information."
14+
}

aws/logs_monitoring_go/internal/config/apikey.go

Lines changed: 0 additions & 89 deletions
This file was deleted.

aws/logs_monitoring_go/internal/config/config.go

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,24 @@ package config
77

88
import (
99
"compress/gzip"
10+
"context"
1011
"errors"
12+
"fmt"
1113
"log/slog"
1214
"os"
1315
"regexp"
1416

1517
"github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go/internal/model"
18+
"github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go/internal/sdkclient"
1619
)
1720

1821
const (
19-
DefaultSite = "datadoghq.com"
20-
DefaultPort = "443"
21-
DefaultProtocol = "https"
22-
DefaultLogLevel = "INFO"
23-
22+
DefaultSite = "datadoghq.com"
23+
DefaultPort = "443"
24+
DefaultProtocol = "https"
25+
DefaultLogLevel = "INFO"
2426
EnvAPIKey = "DD_API_KEY"
2527
EnvSite = "DD_SITE"
26-
EnvURL = "DD_URL"
2728
EnvAPIURL = "DD_API_URL"
2829
EnvLogLevel = "DD_LOG_LEVEL"
2930
EnvPort = "DD_PORT"
@@ -45,6 +46,17 @@ const (
4546
ForwarderVersion = "6.0"
4647
)
4748

49+
type apiKeyResolver struct {
50+
env string
51+
resolve func(ctx context.Context, value string) (string, error)
52+
}
53+
54+
var apiKeyResolvers = []apiKeyResolver{
55+
{"DD_API_KEY_SECRET_ARN", sdkclient.ResolveFromSecretsManager},
56+
{"DD_API_KEY_SSM_NAME", sdkclient.ResolveFromSSM},
57+
{"DD_KMS_API_KEY", sdkclient.ResolveFromKMS},
58+
}
59+
4860
type Config struct {
4961
APIKey string
5062
APIURL string
@@ -92,6 +104,10 @@ func Load() (*Config, error) {
92104
}
93105
}
94106

107+
var resolutionErr error
108+
cfg.APIKey, resolutionErr = resolveAPIKey(context.Background(), apiKeyResolvers)
109+
errs = append(errs, resolutionErr)
110+
95111
return &cfg, errors.Join(errs...)
96112
}
97113

@@ -113,8 +129,7 @@ func (c *Config) loadEnv() {
113129
}
114130
c.CompressionLevel = compressionLevel
115131

116-
c.IntakeURL = envOrDefault(EnvURL, protocol+"://http-intake.logs."+site+":"+port+"/api/v2/logs")
117-
c.APIURL = envOrDefault(EnvAPIURL, protocol+"://api."+site)
132+
c.IntakeURL, c.APIURL = buildURLs(protocol, site, port)
118133

119134
c.Source = envOrDefault(EnvSource, "")
120135
c.Host = envOrDefault(EnvHost, "")
@@ -126,3 +141,27 @@ func (c *Config) loadEnv() {
126141
c.ScrubIP = envOrDefaultBool(EnvRedactIP, false)
127142
c.ScrubEmail = envOrDefaultBool(EnvRedactEmail, false)
128143
}
144+
145+
func buildURLs(protocol, site, port string) (intakeURL string, apiURL string) {
146+
intakeURL = protocol + "://http-intake.logs." + site + ":" + port + "/api/v2/logs"
147+
apiURL = protocol + "://api." + site + "/api/v1/validate"
148+
return
149+
}
150+
151+
func resolveAPIKey(ctx context.Context, resolvers []apiKeyResolver) (string, error) {
152+
for _, resolver := range resolvers {
153+
v, ok := os.LookupEnv(resolver.env)
154+
if !ok {
155+
continue
156+
}
157+
158+
key, err := resolver.resolve(ctx, v)
159+
if err != nil {
160+
return "", fmt.Errorf("resolve: %w", err)
161+
}
162+
163+
return key, nil
164+
}
165+
166+
return "", errors.New("no Datadog API key configured")
167+
}

0 commit comments

Comments
 (0)