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
2 changes: 2 additions & 0 deletions cmd/gradle-cache/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type CLI struct {
DatadogAPIKey string `help:"DataDog API key for direct metric submission (no agent required)." env:"DATADOG_API_KEY"`
MetricsTags []string `help:"Additional metric tags in key:value format. May be repeated." name:"metrics-tag"`
CPUProfile string `help:"Write CPU profile to file." name:"cpuprofile" hidden:"" type:"path"`
DDLogFile string `help:"Path to a log file tailed by the Datadog Agent for telemetry events." name:"dd-log-file" env:"DD_LOG_PATH"`
}

type backendFlags struct {
Expand Down Expand Up @@ -266,6 +267,7 @@ func main() {
StatsdAddr: cli.StatsdAddr,
DatadogAPIKey: cli.DatadogAPIKey,
MetricsTags: cli.MetricsTags,
DDLogPath: cli.DDLogFile,
}
metrics := mf.NewMetricsClient()
defer metrics.Close()
Expand Down
34 changes: 34 additions & 0 deletions gradlecache/ddlog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package gradlecache

import (
"log/slog"
"os"
)

// loggedMetrics wraps a MetricsClient and also writes each metric as a JSON
// log line to a file tailed by the Datadog Agent. This allows a single
// Distribution call to emit both a DogStatsD metric and a structured log event.
type loggedMetrics struct {
inner MetricsClient
logger *slog.Logger
file *os.File
}

func newLoggedMetrics(inner MetricsClient, path string) (*loggedMetrics, error) {
f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
if err != nil {
return nil, err
}
logger := slog.New(slog.NewJSONHandler(f, nil))
return &loggedMetrics{inner: inner, logger: logger, file: f}, nil
}

func (m *loggedMetrics) Distribution(name string, value float64, tags ...string) {
m.inner.Distribution(name, value, tags...)
m.logger.Info(name, "value", value, "tags", tags)
}

func (m *loggedMetrics) Close() {
m.inner.Close()
_ = m.file.Close()
}
110 changes: 110 additions & 0 deletions gradlecache/ddlog_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package gradlecache

import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)

func TestLoggedMetrics_WritesJSON(t *testing.T) {
path := filepath.Join(t.TempDir(), "dd.log")

inner := &recordingMetrics{}
lm, err := newLoggedMetrics(inner, path)
if err != nil {
t.Fatal(err)
}

lm.Distribution("gradle_cache.restore.duration_ms", 1234, "cache_key:foo")
lm.Close()

// Verify inner client received the metric.
if len(inner.calls) != 1 {
t.Fatalf("expected 1 inner call, got %d", len(inner.calls))
}
if inner.calls[0].name != "gradle_cache.restore.duration_ms" {
t.Errorf("expected metric name, got %s", inner.calls[0].name)
}

// Verify JSON was written to the file.
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}

var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("expected valid JSON, got: %s", string(data))
}
if m["msg"] != "gradle_cache.restore.duration_ms" {
t.Errorf("expected metric name as msg, got %v", m["msg"])
}
if m["value"] != 1234.0 {
t.Errorf("expected value=1234, got %v", m["value"])
}
}

func TestLoggedMetrics_Appends(t *testing.T) {
path := filepath.Join(t.TempDir(), "dd.log")

inner := NoopMetrics{}
lm, err := newLoggedMetrics(inner, path)
if err != nil {
t.Fatal(err)
}

lm.Distribution("metric.one", 1)
lm.Distribution("metric.two", 2)
lm.Close()

data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}

lines := strings.Split(strings.TrimSpace(string(data)), "\n")
if len(lines) != 2 {
t.Fatalf("expected 2 lines, got %d", len(lines))
}
}

func TestLoggedMetrics_InvalidPath(t *testing.T) {
_, err := newLoggedMetrics(NoopMetrics{}, "/nonexistent/dir/dd.log")
if err == nil {
t.Error("expected error for invalid path")
}
}

func TestLoggedMetrics_ClosesInner(t *testing.T) {
path := filepath.Join(t.TempDir(), "dd.log")
inner := &recordingMetrics{}
lm, err := newLoggedMetrics(inner, path)
if err != nil {
t.Fatal(err)
}
lm.Close()
if !inner.closed {
t.Error("expected inner client to be closed")
}
}

type metricCall struct {
name string
value float64
tags []string
}

type recordingMetrics struct {
calls []metricCall
closed bool
}

func (r *recordingMetrics) Distribution(name string, value float64, tags ...string) {
r.calls = append(r.calls, metricCall{name, value, tags})
}

func (r *recordingMetrics) Close() {
r.closed = true
}
38 changes: 27 additions & 11 deletions gradlecache/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type MetricsFlags struct {
StatsdAddr string
DatadogAPIKey string
MetricsTags []string
DDLogPath string // optional: path to a file tailed by the DD agent for log-based metrics
}

// DetectStatsdAddr returns the DogStatsD address from the environment, or empty
Expand All @@ -47,27 +48,42 @@ func DetectStatsdAddr() string {
}

// NewMetricsClient returns a MetricsClient based on the configured flags.
// When DDLogPath is set, each metric is also written as a JSON log line to
// that file for collection by the Datadog Agent.
func (f *MetricsFlags) NewMetricsClient() MetricsClient {
var client MetricsClient
if f.StatsdAddr != "" {
if c := NewStatsdClient(f.StatsdAddr, f.MetricsTags); c != nil {
slog.Debug("metrics: using DogStatsD", "addr", f.StatsdAddr)
return c
client = c
} else {
slog.Warn("failed to connect to DogStatsD, metrics disabled", "addr", f.StatsdAddr)
client = NoopMetrics{}
}
slog.Warn("failed to connect to DogStatsD, metrics disabled", "addr", f.StatsdAddr)
return NoopMetrics{}
}
if f.DatadogAPIKey != "" {
} else if f.DatadogAPIKey != "" {
slog.Debug("metrics: using Datadog HTTP API")
return NewDatadogAPIClient(f.DatadogAPIKey, f.MetricsTags)
}
if addr := DetectStatsdAddr(); addr != "" {
client = NewDatadogAPIClient(f.DatadogAPIKey, f.MetricsTags)
} else if addr := DetectStatsdAddr(); addr != "" {
if c := NewStatsdClient(addr, f.MetricsTags); c != nil {
slog.Debug("metrics: auto-detected DogStatsD agent", "addr", addr)
return c
client = c
}
}
if client == nil {
slog.Debug("metrics: no backend configured, metrics disabled")
client = NoopMetrics{}
}

if f.DDLogPath != "" {
lm, err := newLoggedMetrics(client, f.DDLogPath)
if err != nil {
slog.Warn("failed to open DD log file, log-based metrics disabled", "path", f.DDLogPath, "error", err)
return client
}
slog.Debug("metrics: also logging to DD agent file", "path", f.DDLogPath)
return lm
}
slog.Debug("metrics: no backend configured, metrics disabled")
return NoopMetrics{}
return client
}

// ── DogStatsD (UDP) ─────────────────────────────────────────────────────────
Expand Down
Loading