Skip to content
Closed
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
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ S3_PREFIX=snapshots/
# ─── HTTP Admin Service ───────────────────────────────────────────────────────
HTTP_PORT=8081

# ─── Metrics ──────────────────────────────────────────────────────────────────
# Leave disabled for local development, or set to dogstatsd to emit metrics to
# a DogStatsD-compatible agent such as Datadog Agent.
METRICS_BACKEND=none
# DOGSTATSD_ADDR defaults to DD_AGENT_HOST:DD_DOGSTATSD_PORT, or 127.0.0.1:8125.
# DOGSTATSD_ADDR=127.0.0.1:8125
# Optional comma-separated tags. service/env/version also come from
# DD_SERVICE/DD_ENV/DD_VERSION when set.
METRICS_TAGS=service:version-guard

# ─── Tag Configuration ────────────────────────────────────────────────────────
# Customize which AWS resource tags to use for extracting metadata
# Comma-separated lists - first match wins
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,23 @@ The `endoflife` service serves patched EOL data for products with pending upstre

Once running, open the Temporal Web UI at http://localhost:8233 to trigger and monitor workflows.

### Metrics

Metrics are disabled by default. To emit scan aggregates to a DogStatsD-compatible
agent such as Datadog Agent, set:

```bash
METRICS_BACKEND=dogstatsd
DOGSTATSD_ADDR=127.0.0.1:8125 # optional
METRICS_TAGS=service:version-guard,team:platform
```

Version Guard emits `version_guard.findings.*`,
`version_guard.compliance_percentage`, `version_guard.detection.duration_ms`,
`version_guard.inventory.*`, and `version_guard.scan.completed`. Standard
Datadog tags are also read from `DD_SERVICE`, `DD_ENV`, and `DD_VERSION` when
present.

### Run Locally (manual)

If you prefer running components individually:
Expand Down
20 changes: 18 additions & 2 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,13 +348,29 @@ temporal workflow observe --workflow-id <workflow-id> --namespace version-guard-

#### Metrics to Track

Version Guard emits the following metrics (if Datadog enabled):
Version Guard emits the following metrics when `METRICS_BACKEND=dogstatsd`:
- `version_guard.findings.red` - Critical issues count
- `version_guard.findings.yellow` - Warning issues count
- `version_guard.findings.green` - Compliant resources count
- `version_guard.findings.unknown` - Resources with unknown lifecycle data
- `version_guard.findings.total` - Total resources scanned
- `version_guard.compliance_percentage` - Fleet compliance %
- `version_guard.detection.duration_ms` - Scan duration
- `version_guard.inventory.fetch` - Inventory fetch success rate
- `version_guard.inventory.fetch` - Inventory fetch success (1) or failure (0)
- `version_guard.inventory.resources` - Resources returned by a successful inventory fetch
- `version_guard.scan.completed` - Completed scans count

DogStatsD emission is disabled by default for local and OSS users. Enable it
with:

```bash
METRICS_BACKEND=dogstatsd
DOGSTATSD_ADDR=127.0.0.1:8125 # optional; defaults to DD_AGENT_HOST/DD_DOGSTATSD_PORT or 127.0.0.1:8125
METRICS_TAGS=service:version-guard,team:platform
```

If `DD_SERVICE`, `DD_ENV`, or `DD_VERSION` are set, Version Guard adds those as
metric tags unless the same tag key is already present in `METRICS_TAGS`.

#### Logs

Expand Down
97 changes: 96 additions & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
Expand All @@ -22,6 +23,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/s3"

vgconfig "github.com/block/Version-Guard/pkg/config"
"github.com/block/Version-Guard/pkg/emitters"
"github.com/block/Version-Guard/pkg/eol"
eolendoflife "github.com/block/Version-Guard/pkg/eol/endoflife"
"github.com/block/Version-Guard/pkg/inventory"
Expand Down Expand Up @@ -70,6 +72,11 @@ type ServerCLI struct {
// Service configuration
HTTPPort int `help:"HTTP admin port (POST /scan)" default:"8081" env:"HTTP_PORT"`

// Metrics configuration
MetricsBackend string `help:"Metrics backend: none or dogstatsd" default:"none" env:"METRICS_BACKEND"`
DogStatsDAddr string `help:"DogStatsD UDP address (defaults to DD_AGENT_HOST:DD_DOGSTATSD_PORT or 127.0.0.1:8125)" env:"DOGSTATSD_ADDR"`
MetricsTags string `help:"Comma-separated metric tags, e.g. service:version-guard,team:platform" env:"METRICS_TAGS"`

// Tag configuration (comma-separated lists for AWS resource tags)
TagAppKeys string `help:"Comma-separated tag keys for application/service name" default:"app,application,service" env:"TAG_APP_KEYS"`

Expand All @@ -93,6 +100,10 @@ type ServerCLI struct {

// parseTagKeys parses a comma-separated string into a slice of tag keys
func parseTagKeys(input string) []string {
return parseCSV(input)
}

func parseCSV(input string) []string {
if input == "" {
return []string{}
}
Expand All @@ -107,13 +118,81 @@ func parseTagKeys(input string) []string {
return result
}

func parseMetricTags(input string) []string {
tags := parseCSV(input)
result := make([]string, 0, len(tags))
for _, tag := range tags {
if strings.Contains(tag, ":") {
result = append(result, tag)
}
}
return result
}

func metricTagKeyExists(tags []string, key string) bool {
prefix := key + ":"
for _, tag := range tags {
if strings.HasPrefix(tag, prefix) {
return true
}
}
return false
}

func appendMetricTagIfMissing(tags []string, key, value string) []string {
if value == "" || metricTagKeyExists(tags, key) {
return tags
}
return append(tags, key+":"+value)
}

func buildMetricTags(input string) []string {
tags := parseMetricTags(input)
tags = appendMetricTagIfMissing(tags, "service", os.Getenv("DD_SERVICE"))
tags = appendMetricTagIfMissing(tags, "service", "version-guard")
tags = appendMetricTagIfMissing(tags, "env", os.Getenv("DD_ENV"))
tags = appendMetricTagIfMissing(tags, "version", os.Getenv("DD_VERSION"))
return tags
}

func dogStatsDAddr(configured string) string {
if configured != "" {
return configured
}
host := os.Getenv("DD_AGENT_HOST")
if host == "" {
host = "127.0.0.1"
}
port := os.Getenv("DD_DOGSTATSD_PORT")
if port == "" {
port = "8125"
}
return net.JoinHostPort(host, port)
}

// buildTagConfig creates a TagConfig from the environment variables
func (s *ServerCLI) buildTagConfig() *wiz.TagConfig {
return &wiz.TagConfig{
AppTags: parseTagKeys(s.TagAppKeys),
}
}

func (s *ServerCLI) buildMetricsEmitter() (emitters.MetricsEmitter, func() error, error) {
switch strings.ToLower(strings.TrimSpace(s.MetricsBackend)) {
case "", "none", "noop":
return emitters.NoopMetricsEmitter{}, nil, nil
case "dogstatsd", "datadog":
addr := dogStatsDAddr(s.DogStatsDAddr)
metricsEmitter, err := emitters.NewDogStatsDMetricsEmitter(addr, buildMetricTags(s.MetricsTags))
if err != nil {
return nil, nil, err
}
return metricsEmitter, metricsEmitter.Close, nil
default:
return nil, nil, fmt.Errorf("unsupported metrics backend %q", s.MetricsBackend)
}
}

//nolint:gocognit,gocyclo // startup wires many optional components; splitting further would fragment a linear init sequence
func (s *ServerCLI) Run(_ *kong.Context) error {
// Initialize structured logger
Expand All @@ -138,6 +217,7 @@ func (s *ServerCLI) Run(_ *kong.Context) error {
fmt.Printf(" Wiz Cache TTL: %d hours\n", s.WizCacheTTLHours)
fmt.Printf(" AWS Region: %s\n", s.AWSRegion)
fmt.Printf(" S3 Prefix: %s\n", s.S3Prefix)
fmt.Printf(" Metrics Backend: %s\n", s.MetricsBackend)
fmt.Printf(" Tag Keys - App: %s\n", s.TagAppKeys)
if s.ScheduleEnabled {
fmt.Printf(" Schedule: enabled (cron: %s, id: %s, jitter: %s)\n",
Expand All @@ -156,6 +236,21 @@ func (s *ServerCLI) Run(_ *kong.Context) error {
st := memory.NewStore()
fmt.Println("✓ In-memory store initialized")

metricsEmitter, closeMetricsEmitter, err := s.buildMetricsEmitter()
if err != nil {
return fmt.Errorf("failed to configure metrics: %w", err)
}
if closeMetricsEmitter != nil {
defer func() {
if closeErr := closeMetricsEmitter(); closeErr != nil {
fmt.Printf("metrics emitter shutdown error: %v\n", closeErr)
}
}()
fmt.Printf("✓ Metrics configured (backend: %s, address: %s)\n", s.MetricsBackend, dogStatsDAddr(s.DogStatsDAddr))
} else {
fmt.Println("✓ Metrics disabled")
}

// Initialize S3 snapshot store
var snapshotStore *snapshot.S3Store
ctx := context.Background()
Expand Down Expand Up @@ -357,7 +452,7 @@ func (s *ServerCLI) Run(_ *kong.Context) error {
eolProviders,
policyEngine,
st,
)
).WithMetricsEmitter(metricsEmitter)
w.RegisterActivityWithOptions(detectionActivities.FetchInventory, activity.RegisterOptions{Name: detection.FetchInventoryActivityName})
w.RegisterActivityWithOptions(detectionActivities.FetchEOLData, activity.RegisterOptions{Name: detection.FetchEOLDataActivityName})
w.RegisterActivityWithOptions(detectionActivities.DetectDrift, activity.RegisterOptions{Name: detection.DetectDriftActivityName})
Expand Down
54 changes: 54 additions & 0 deletions cmd/server/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package main

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestBuildMetricTagsDefaultsService(t *testing.T) {
t.Setenv("DD_SERVICE", "")
t.Setenv("DD_ENV", "")
t.Setenv("DD_VERSION", "")

assert.Equal(t, []string{"service:version-guard"}, buildMetricTags(""))
}

func TestBuildMetricTagsUsesDatadogEnvVars(t *testing.T) {
t.Setenv("DD_SERVICE", "custom-service")
t.Setenv("DD_ENV", "staging")
t.Setenv("DD_VERSION", "1.2.3")

assert.Equal(t, []string{
"team:platform",
"service:custom-service",
"env:staging",
"version:1.2.3",
}, buildMetricTags("team:platform"))
}

func TestBuildMetricTagsDoesNotOverrideExplicitTags(t *testing.T) {
t.Setenv("DD_SERVICE", "env-service")
t.Setenv("DD_ENV", "prod")
t.Setenv("DD_VERSION", "1.2.3")

assert.Equal(t, []string{
"service:explicit-service",
"env:explicit-env",
"version:explicit-version",
}, buildMetricTags("service:explicit-service,env:explicit-env,version:explicit-version"))
}

func TestDogStatsDAddrDefaults(t *testing.T) {
t.Setenv("DD_AGENT_HOST", "")
t.Setenv("DD_DOGSTATSD_PORT", "")

assert.Equal(t, "127.0.0.1:8125", dogStatsDAddr(""))
}

func TestDogStatsDAddrUsesDatadogEnvVars(t *testing.T) {
t.Setenv("DD_AGENT_HOST", "datadog-agent")
t.Setenv("DD_DOGSTATSD_PORT", "8126")

assert.Equal(t, "datadog-agent:8126", dogStatsDAddr(""))
}
10 changes: 6 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/block/Version-Guard
go 1.24.2

require (
github.com/DataDog/datadog-go/v5 v5.8.3
github.com/alecthomas/kong v1.15.0
github.com/aws/aws-sdk-go-v2 v1.41.5
github.com/aws/aws-sdk-go-v2/config v1.32.14
Expand All @@ -17,6 +18,7 @@ require (
)

require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.14 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
Expand All @@ -33,21 +35,21 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/mock v1.7.0-rc.1 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
github.com/nexus-rpc/sdk-go v0.6.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/robfig/cron v1.2.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
go.temporal.io/api v1.62.7 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/time v0.12.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
google.golang.org/protobuf v1.36.11 // indirect
Expand Down
Loading
Loading