Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 31 additions & 10 deletions aws/logs_monitoring_go/Makefile
Original file line number Diff line number Diff line change
@@ -1,33 +1,54 @@
.PHONY: build package test lint clean sam-build sam-invoke sam-deploy build-ForwarderFunction

BINARY_NAME := bootstrap
ZIP_NAME := forwarder.zip

build:
GOOS=linux GOARCH=arm64 go build -o $(BINARY_NAME) ./cmd/forwarder/
# Go

# Used by `sam build`
build-ForwarderFunction:
GOOS=linux GOARCH=arm64 go build -o $(ARTIFACTS_DIR)/bootstrap ./cmd/forwarder/
.PHONY: build
build:
GOOS=linux GOARCH=arm64 go build -ldflags="-s" -installsuffix nocgo -o $(BINARY_NAME) ./cmd/forwarder/

.PHONY: package
package: build
zip $(ZIP_NAME) $(BINARY_NAME)

.PHONY: test
test:
go test -race ./...

.PHONY: lint
lint:
golangci-lint run ./...

.PHONY: audit
audit:
go vet ./...
go tool govulncheck

.PHONY: generate
generate:
go generate ./...

.PHONY: clean
clean:
rm -f $(BINARY_NAME) $(ZIP_NAME)

# SAM

# Used only by SAM
.PHONY: build-ForwarderFunction
build-ForwarderFunction:
GOOS=linux GOARCH=arm64 go build -ldflags="-s" -installsuffix nocgo -o $(ARTIFACTS_DIR)/bootstrap ./cmd/forwarder/

.PHONY: sam-build
sam-build:
sam build

.PHONY: sam-deploy
sam-deploy: sam-build
sam deploy

EVENT ?= events/cloudwatch_logs.json

.PHONY: sam-invoke
sam-invoke: sam-build
sam local invoke ForwarderFunction -e $(EVENT)

sam-deploy: sam-build
sam deploy
24 changes: 18 additions & 6 deletions aws/logs_monitoring_go/cmd/forwarder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,28 @@ package main
import (
"context"
Comment thread
ndakkoune marked this conversation as resolved.
"encoding/json"
"log"
"log/slog"

"github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go/internal/config"

"github.com/aws/aws-lambda-go/lambda"
)

func handleRequest(ctx context.Context, event json.RawMessage) error {
log.Printf("Received event: %s", string(event))
return nil
func main() {
ctx := context.Background()
cfg, err := config.Load(ctx)
if err != nil {
slog.Error("config load failed", slog.Any("error", err))
return
}

lambda.Start(handleRequest(cfg))
}

func main() {
lambda.Start(handleRequest)
// cfg not used for now, will be when forwarding logic added
func handleRequest(cfg *config.Config) func(context.Context, json.RawMessage) error {
return func(ctx context.Context, event json.RawMessage) error {
slog.Info("received event", slog.String("event", string(event)))
return nil
}
}
37 changes: 36 additions & 1 deletion aws/logs_monitoring_go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,39 @@ module github.com/DataDog/datadog-serverless-functions/aws/logs_monitoring_go

go 1.26

require github.com/aws/aws-lambda-go v1.53.0
require (
github.com/aws/aws-lambda-go v1.53.0
github.com/aws/aws-sdk-go-v2 v1.41.4
github.com/aws/aws-sdk-go-v2/config v1.32.12
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.4
github.com/google/go-cmp v0.7.0
)

require (
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/stretchr/testify v1.11.1 // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect
golang.org/x/tools v0.43.0 // indirect
golang.org/x/vuln v1.1.4 // indirect
)

tool (
go.uber.org/mock/mockgen
golang.org/x/tools/cmd/goimports
golang.org/x/vuln/cmd/govulncheck
)
56 changes: 54 additions & 2 deletions aws/logs_monitoring_go/go.sum
Original file line number Diff line number Diff line change
@@ -1,10 +1,62 @@
github.com/aws/aws-lambda-go v1.53.0 h1:uAMv6W/vCP/L494BAUSxe+8KVBIPK+SGPyapFt3FuMk=
github.com/aws/aws-lambda-go v1.53.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A=
github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k=
github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=
github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.4 h1:9aZbO86sraeCIHHCpZhxwN9tnVy9POkSKzi4/TpT54A=
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.4/go.mod h1:cxiXDhEzIq7Xx1BtmC4lGBK3SwAZ79+EUWiKawYHo14=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc=
golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I=
golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
103 changes: 103 additions & 0 deletions aws/logs_monitoring_go/internal/config/apikey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2026-Present Datadog, Inc.

package config

import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"time"

awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
)

var (
ErrMissingAPIKey = errors.New("missing Datadog API key")
ErrInvalidAPIKey = errors.New("invalid Datadog API key format")
)

const (
httpClientTimeout = 5 * time.Second
)

func (c *Config) resolveAPIKey(ctx context.Context) error {
awsCfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithHTTPClient(awshttp.NewBuildableClient().WithTimeout(httpClientTimeout)))
if err != nil {
return fmt.Errorf("loading AWS config: %w", err)
}

if v, ok := os.LookupEnv("DD_API_KEY_SECRET_ARN"); ok {
client, err := c.createSecretsManagerAPIClient(ctx, awsCfg)
if err != nil {
return fmt.Errorf("creating Secrets Manager client: %w", err)
}

apiKey, err := resolveFromSecretsManager(ctx, client, v)
if err != nil {
return fmt.Errorf("resolving from secrets manager: %w", err)
}

c.APIKey = apiKey
return nil
}

if v, ok := os.LookupEnv("DD_API_KEY_SSM_NAME"); ok {
return c.resolveFromSSM(ctx, awsCfg, v)
}

if v, ok := os.LookupEnv("DD_KMS_API_KEY"); ok {
return c.resolveFromKMS(ctx, awsCfg, v)
}

return errors.New("no API key configured: set DD_API_KEY_SECRET_ARN, DD_API_KEY_SSM_NAME or DD_KMS_API_KEY. See: https://docs.datadoghq.com/serverless/forwarder/")
}

func (c *Config) validateAPIKey(ctx context.Context) error {
if c.APIKey == "" {
return fmt.Errorf("set DD_API_KEY_SECRET_ARN, DD_API_KEY_SSM_NAME or DD_KMS_API_KEY. See: https://docs.datadoghq.com/serverless/forwarder/: %w", ErrMissingAPIKey)
}

if len(c.APIKey) != 32 {
return fmt.Errorf("expected 32 characters, got %d. Verify your API key at https://app.%s/organization-settings/api-keys: %w", len(c.APIKey), c.Site, ErrInvalidAPIKey)
}

slog.Debug("validating Datadog API key")

req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.APIURL+"/api/v1/validate", nil)
if err != nil {
slog.Warn("failed to build API key validation request", slog.Any("error", err))
return nil
}
req.Header.Set("DD-API-KEY", c.APIKey)

client := &http.Client{Timeout: httpClientTimeout}
resp, err := client.Do(req)
if err != nil {
slog.Warn("failed to validate API key", slog.Any("error", err))
return nil
}
defer func() {
if _, err := io.Copy(io.Discard, resp.Body); err != nil {
slog.Warn("failed to drain response body", slog.Any("error", err))
}
if err := resp.Body.Close(); err != nil {
slog.Warn("failed to close response body", slog.Any("error", err))
}
}()

if resp.StatusCode == http.StatusForbidden {
slog.Warn("invalid Datadog API key", slog.String("url", "https://app."+c.Site+"/organization-settings/api-keys"))
} else if resp.StatusCode != http.StatusOK {
slog.Warn("unexpected response from validation endpoint", slog.String("status", resp.Status))
}

return nil
}
50 changes: 50 additions & 0 deletions aws/logs_monitoring_go/internal/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2026-Present Datadog, Inc.

package config

import (
"context"
"fmt"
"log/slog"
)

type Config struct {
APIKey string
Site string
IntakeURL string
APIURL string
LogLevel string
UseFIPS bool
}

func Load(ctx context.Context) (*Config, error) {
initLogger(envOrDefault("DD_LOG_LEVEL", "INFO"))
logDroppedEnvVars()

cfg := loadConfig()
slog.Debug("config loaded", slog.String("site", cfg.Site), slog.String("intakeURL", cfg.IntakeURL), slog.String("apiURL", cfg.APIURL), slog.String("logLevel", cfg.LogLevel), slog.Bool("useFIPS", cfg.UseFIPS))

if err := cfg.resolveAPIKey(ctx); err != nil {
return nil, fmt.Errorf("resolving API key: %w", err)
}

Comment on lines +26 to +33
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be one function

if err := cfg.validateAPIKey(ctx); err != nil {
return nil, fmt.Errorf("validating API key: %w", err)
}

return cfg, nil
}

func loadConfig() *Config {
site := envOrDefault("DD_SITE", "datadoghq.com")
return &Config{
Site: site,
IntakeURL: envOrDefault("DD_URL", "https://http-intake.logs."+site),
APIURL: envOrDefault("DD_API_URL", "https://api."+site),
LogLevel: envOrDefault("DD_LOG_LEVEL", "INFO"),
UseFIPS: envOrDefaultBool("DD_USE_FIPS", false),
}
}
Loading
Loading