diff --git a/cmd/agent/subcommands/run/command.go b/cmd/agent/subcommands/run/command.go index fbc7f1621dfa29..1da126b6a7afc2 100644 --- a/cmd/agent/subcommands/run/command.go +++ b/cmd/agent/subcommands/run/command.go @@ -115,6 +115,7 @@ import ( "github.com/DataDog/datadog-agent/comp/otelcol" otelcollector "github.com/DataDog/datadog-agent/comp/otelcol/collector/def" "github.com/DataDog/datadog-agent/comp/otelcol/logsagentpipeline" + otelagentStatusfx "github.com/DataDog/datadog-agent/comp/otelcol/status/fx" "github.com/DataDog/datadog-agent/comp/process" processAgent "github.com/DataDog/datadog-agent/comp/process/agent" processagentStatusImpl "github.com/DataDog/datadog-agent/comp/process/status/statusimpl" @@ -374,6 +375,7 @@ func getSharedFxOption() fx.Option { AgentVersion: version.AgentVersion, }, ), + otelagentStatusfx.Module(), traceagentStatusImpl.Module(), processagentStatusImpl.Module(), dogstatsdStatusimpl.Module(), diff --git a/comp/README.md b/comp/README.md index e496f419c6d46a..c8e3bf4468333f 100644 --- a/comp/README.md +++ b/comp/README.md @@ -416,6 +416,10 @@ Package ddflareextension defines the OpenTelemetry Extension component. Package logsagentpipeline contains logs agent pipeline component +### [comp/otelcol/status](https://pkg.go.dev/github.com/DataDog/datadog-agent/comp/otelcol/status) + +Package status implements the core status component information provider interface + ## [comp/process](https://pkg.go.dev/github.com/DataDog/datadog-agent/comp/process) (Component Bundle) *Datadog Team*: container-intake diff --git a/comp/otelcol/status/def/component.go b/comp/otelcol/status/def/component.go new file mode 100644 index 00000000000000..0ea47afbfe9922 --- /dev/null +++ b/comp/otelcol/status/def/component.go @@ -0,0 +1,13 @@ +// 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 2025-present Datadog, Inc. + +// Package status implements the core status component information provider interface +package status + +// team: opentelemetry opentelemetry-agent + +// Component is the status interface. +type Component interface { +} diff --git a/comp/otelcol/status/fx/fx.go b/comp/otelcol/status/fx/fx.go new file mode 100644 index 00000000000000..491e7d3d21bcdd --- /dev/null +++ b/comp/otelcol/status/fx/fx.go @@ -0,0 +1,21 @@ +// 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 2025-present Datadog, Inc. + +// Package fx provides the fx module for the status component +package fx + +import ( + statusimpl "github.com/DataDog/datadog-agent/comp/otelcol/status/impl" + "github.com/DataDog/datadog-agent/pkg/util/fxutil" +) + +// Module defines the fx options for the status component. +func Module() fxutil.Module { + return fxutil.Component( + fxutil.ProvideComponentConstructor( + statusimpl.NewComponent, + ), + ) +} diff --git a/comp/otelcol/status/impl/status.go b/comp/otelcol/status/impl/status.go new file mode 100644 index 00000000000000..b0bc3c8cb42988 --- /dev/null +++ b/comp/otelcol/status/impl/status.go @@ -0,0 +1,228 @@ +// 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 2025-present Datadog, Inc. + +// Package statusimpl implements the status component interface +package statusimpl + +import ( + "embed" + "encoding/json" + "fmt" + "gopkg.in/yaml.v3" + "io" + "net/http" + + "github.com/DataDog/datadog-agent/comp/core/config" + statusComponent "github.com/DataDog/datadog-agent/comp/core/status" + ddflareextension "github.com/DataDog/datadog-agent/comp/otelcol/ddflareextension/def" + status "github.com/DataDog/datadog-agent/comp/otelcol/status/def" + apiutil "github.com/DataDog/datadog-agent/pkg/api/util" + "github.com/DataDog/datadog-agent/pkg/util/prometheus" +) + +//go:embed status_templates +var templatesFS embed.FS + +// Requires defines the dependencies of the status component. +type Requires struct { + Config config.Component +} + +// Provides contains components provided by status constructor. +type Provides struct { + Comp status.Component + StatusProvider statusComponent.InformationProvider +} + +type statusProvider struct { + Config config.Component + client *http.Client + receiverStatus map[string]interface{} + exporterStatus map[string]interface{} +} + +type prometheusRuntimeConfig struct { + Service struct { + Telemetry struct { + Metrics struct { + Readers []struct { + Pull struct { + Exporter struct { + Prometheus struct { + Host string + Port int + } + } + } + } + } + } + } +} + +// NewComponent creates a new status component. +func NewComponent(reqs Requires) Provides { + comp := statusProvider{ + Config: reqs.Config, + client: apiutil.GetClient(false), + receiverStatus: map[string]interface{}{ + "spans": 0.0, + "metrics": 0.0, + "logs": 0.0, + "refused_spans": 0.0, + "refused_metrics": 0.0, + "refused_logs": 0.0, + }, + exporterStatus: map[string]interface{}{ + "spans": 0.0, + "metrics": 0.0, + "logs": 0.0, + "failed_spans": 0.0, + "failed_metrics": 0.0, + "failed_logs": 0.0, + }, + } + return Provides{ + Comp: comp, + StatusProvider: statusComponent.NewInformationProvider(comp), + } +} + +// Name returns the name +func (s statusProvider) Name() string { + return "OTel Agent" +} + +// Section return the section +func (s statusProvider) Section() string { + return "OTel Agent" +} + +func (s statusProvider) getStatusInfo() map[string]interface{} { + statusInfo := make(map[string]interface{}) + + values := s.populateStatus() + + statusInfo["otelAgent"] = values + + return statusInfo +} + +func getPrometheusURL(extensionResp ddflareextension.Response) (string, error) { + var runtimeConfig prometheusRuntimeConfig + if err := yaml.Unmarshal([]byte(extensionResp.RuntimeConfig), &runtimeConfig); err != nil { + return "", err + } + prometheusHost := "localhost" + prometheusPort := 8888 + for _, reader := range runtimeConfig.Service.Telemetry.Metrics.Readers { + prometheusEndpoint := reader.Pull.Exporter.Prometheus + if prometheusEndpoint.Host != "" && prometheusEndpoint.Port != 0 { + prometheusHost = prometheusEndpoint.Host + prometheusPort = prometheusEndpoint.Port + break + } + } + return fmt.Sprintf("http://%v:%d/metrics", prometheusHost, prometheusPort), nil +} + +func (s statusProvider) populatePrometheusStatus(prometheusURL string) error { + resp, err := apiutil.DoGet(s.client, prometheusURL, apiutil.CloseConnection) + if err != nil { + return err + } + metrics, err := prometheus.ParseMetrics(resp) + if err != nil { + return err + } + + for _, m := range metrics { + value := m.Samples[0].Value + switch m.Name { + case "otelcol_receiver_accepted_spans": + s.receiverStatus["spans"] = value + case "otelcol_receiver_accepted_metric_points": + s.receiverStatus["metrics"] = value + case "otelcol_receiver_accepted_log_records": + s.receiverStatus["logs"] = value + case "otelcol_receiver_refused_spans": + s.receiverStatus["refused_spans"] = value + case "otelcol_receiver_refused_metric_points": + s.receiverStatus["refused_metrics"] = value + case "otelcol_receiver_refused_log_records": + s.receiverStatus["refused_logs"] = value + case "otelcol_exporter_sent_spans": + s.exporterStatus["spans"] = value + case "otelcol_exporter_sent_metric_points": + s.exporterStatus["metrics"] = value + case "otelcol_exporter_sent_log_records": + s.exporterStatus["logs"] = value + case "otelcol_exporter_send_failed_spans": + s.exporterStatus["failed_spans"] = value + case "otelcol_exporter_send_failed_metric_points": + s.exporterStatus["failed_metrics"] = value + case "otelcol_exporter_send_failed_log_records": + s.exporterStatus["failed_logs"] = value + } + } + return nil +} + +func (s statusProvider) populateStatus() map[string]interface{} { + extensionURL := s.Config.GetString("otelcollector.extension_url") + resp, err := apiutil.DoGet(s.client, extensionURL, apiutil.CloseConnection) + if err != nil { + return map[string]interface{}{ + "url": extensionURL, + "error": err.Error(), + } + } + var extensionResp ddflareextension.Response + if err = json.Unmarshal(resp, &extensionResp); err != nil { + return map[string]interface{}{ + "url": extensionURL, + "error": err.Error(), + } + } + prometheusURL, err := getPrometheusURL(extensionResp) + if err != nil { + return map[string]interface{}{ + "url": extensionURL, + "error": err.Error(), + } + } + err = s.populatePrometheusStatus(prometheusURL) + if err != nil { + return map[string]interface{}{ + "url": prometheusURL, + "error": err.Error(), + } + } + return map[string]interface{}{ + "agentVersion": extensionResp.AgentVersion, + "collectorVersion": extensionResp.ExtensionVersion, + "receiver": s.receiverStatus, + "exporter": s.exporterStatus, + } +} + +// JSON populates the status map +func (s statusProvider) JSON(_ bool, stats map[string]interface{}) error { + values := s.populateStatus() + + stats["otelAgent"] = values + + return nil +} + +// Text renders the text output +func (s statusProvider) Text(_ bool, buffer io.Writer) error { + return statusComponent.RenderText(templatesFS, "otelagent.tmpl", buffer, s.getStatusInfo()) +} + +// HTML renders the html output +func (s statusProvider) HTML(_ bool, buffer io.Writer) error { + return statusComponent.RenderHTML(templatesFS, "otelagentHTML.tmpl", buffer, s.getStatusInfo()) +} diff --git a/comp/otelcol/status/impl/status_templates/otelagent.tmpl b/comp/otelcol/status/impl/status_templates/otelagent.tmpl new file mode 100644 index 00000000000000..0d00e7f7945575 --- /dev/null +++ b/comp/otelcol/status/impl/status_templates/otelagent.tmpl @@ -0,0 +1,29 @@ +{{- with .otelAgent }} +{{- if .error }} + + Status: Not running or unreachable on {{.url}}. + Error: {{.error}} +{{- else}} + Status: Running + {{if .agentVersion}}Agent Version: {{.agentVersion}} {{end}} + {{if .collectorVersion}}Collector Version: {{.collectorVersion}} {{end}} + + Receiver + ========================== + Spans Accepted: {{.receiver.spans}} + {{- if gt .receiver.refused_spans 0.0}}, WARNING: Refused spans: {{.receiver.refused_spans}}{{end}} + Metric Points Accepted: {{.receiver.metrics}} + {{- if gt .receiver.refused_metrics 0.0}}, WARNING: Refused metric points: {{.receiver.refused_metrics}}{{end}} + Log Records Accepted: {{.receiver.logs}} + {{- if gt .receiver.refused_logs 0.0}}, WARNING: Refused log records: {{.receiver.refused_logs}}{{end}} + + Exporter + ========================== + Spans Sent: {{.exporter.spans}} + {{- if gt .exporter.failed_spans 0.0}}, WARNING: Send failed spans: {{.exporter.failed_spans}}{{end}} + Metric Points Sent: {{.exporter.metrics}} + {{- if gt .exporter.failed_metrics 0.0}}, WARNING: Send failed metrics: {{.exporter.failed_metrics}}{{end}} + Log Records Sent: {{.exporter.logs}} + {{- if gt .exporter.failed_logs 0.0}}, WARNING: Send failed logs: {{.exporter.failed_logs}}{{end}} +{{- end}} +{{- end}} diff --git a/comp/otelcol/status/impl/status_templates/otelagentHTML.tmpl b/comp/otelcol/status/impl/status_templates/otelagentHTML.tmpl new file mode 100644 index 00000000000000..2be1dae02d0d58 --- /dev/null +++ b/comp/otelcol/status/impl/status_templates/otelagentHTML.tmpl @@ -0,0 +1,32 @@ +
+ OTel Agent + + {{- with .otelAgent -}} + {{- if .error }} + Not running or unreachable on {{.url}}. + Error: {{.error}}
+ {{- else}} + Status: Running
+ {{if .agentVersion}}Agent Version: {{.agentVersion}}
{{end}} + {{if .collectorVersion}}Collector Version: {{.collectorVersion}}
{{end}} + Receiver + + Spans Accepted: {{.receiver.spans}} + {{- if gt .receiver.refused_spans 0.0}}, WARNING: Refused spans: {{.receiver.refused_spans}}{{end}} + Metric Points Accepted: {{.receiver.metrics}} + {{- if gt .receiver.refused_metrics 0.0}}, WARNING: Refused metric points: {{.receiver.refused_metrics}}{{end}} + Log Records Accepted: {{.receiver.logs}} + {{- if gt .receiver.refused_logs 0.0}}, WARNING: Refused log records: {{.receiver.refused_logs}}{{end}} + Exporter + + Spans Sent: {{.exporter.spans}} + {{- if gt .exporter.failed_spans 0.0}}, WARNING: Send failed spans: {{.exporter.failed_spans}}{{end}} + Metric Points Sent: {{.exporter.metrics}} + {{- if gt .exporter.failed_metrics 0.0}}, WARNING: Send failed metrics: {{.exporter.failed_metrics}}{{end}} + Log Records Sent: {{.exporter.logs}} + {{- if gt .exporter.failed_logs 0.0}}, WARNING: Send failed logs: {{.exporter.failed_logs}}{{end}} + + {{- end }} + {{ end }} + +
diff --git a/comp/otelcol/status/impl/status_test.go b/comp/otelcol/status/impl/status_test.go new file mode 100644 index 00000000000000..f11cefd7eed973 --- /dev/null +++ b/comp/otelcol/status/impl/status_test.go @@ -0,0 +1,56 @@ +// 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 2025-present Datadog, Inc. + +package statusimpl + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/DataDog/datadog-agent/comp/core/config" + "github.com/DataDog/datadog-agent/comp/core/status" +) + +func TestStatusOut(t *testing.T) { + tests := []struct { + name string + assertFunc func(t *testing.T, headerProvider status.Provider) + }{ + {"JSON", func(t *testing.T, headerProvider status.Provider) { + stats := make(map[string]interface{}) + headerProvider.JSON(false, stats) + + assert.NotEmpty(t, stats) + }}, + {"Text", func(t *testing.T, headerProvider status.Provider) { + b := new(bytes.Buffer) + err := headerProvider.Text(false, b) + + assert.NoError(t, err) + + assert.NotEmpty(t, b.String()) + }}, + {"HTML", func(t *testing.T, headerProvider status.Provider) { + b := new(bytes.Buffer) + err := headerProvider.HTML(false, b) + + assert.NoError(t, err) + + assert.NotEmpty(t, b.String()) + }}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + provides := NewComponent(Requires{ + Config: config.NewMock(t), + }) + headerProvider := provides.StatusProvider.Provider + test.assertFunc(t, headerProvider) + }) + } +}