diff --git a/cmd/otel-agent/command/command.go b/cmd/otel-agent/command/command.go index 2a4ea3d6bac0a5..6a2c95c61f71d2 100644 --- a/cmd/otel-agent/command/command.go +++ b/cmd/otel-agent/command/command.go @@ -20,6 +20,7 @@ import ( "github.com/DataDog/datadog-agent/cmd/otel-agent/subcommands" "github.com/DataDog/datadog-agent/cmd/otel-agent/subcommands/run" + "github.com/DataDog/datadog-agent/cmd/otel-agent/subcommands/status" "github.com/DataDog/datadog-agent/pkg/cli/subcommands/version" "go.opentelemetry.io/collector/featuregate" ) @@ -49,6 +50,7 @@ func makeCommands(globalParams *subcommands.GlobalParams) *cobra.Command { commands := []*cobra.Command{ run.MakeCommand(globalConfGetter), version.MakeCommand("otel-agent"), + status.MakeCommand(globalConfGetter), } otelAgentCmd := *commands[0] // root cmd is `run()`; indexed at 0 diff --git a/cmd/otel-agent/subcommands/status/command.go b/cmd/otel-agent/subcommands/status/command.go new file mode 100644 index 00000000000000..f60d7191ba98be --- /dev/null +++ b/cmd/otel-agent/subcommands/status/command.go @@ -0,0 +1,72 @@ +// 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 + +import ( + "os" + + "github.com/spf13/cobra" + "go.uber.org/fx" + + "github.com/DataDog/datadog-agent/cmd/otel-agent/subcommands" + "github.com/DataDog/datadog-agent/comp/api/authtoken/fetchonlyimpl" + coreconfig "github.com/DataDog/datadog-agent/comp/core/config" + log "github.com/DataDog/datadog-agent/comp/core/log/def" + logfx "github.com/DataDog/datadog-agent/comp/core/log/fx" + "github.com/DataDog/datadog-agent/comp/core/secrets" + "github.com/DataDog/datadog-agent/comp/core/secrets/secretsimpl" + status "github.com/DataDog/datadog-agent/comp/otelcol/status/def" + otelagentStatusfx "github.com/DataDog/datadog-agent/comp/otelcol/status/fx" + "github.com/DataDog/datadog-agent/pkg/util/fxutil" + "github.com/DataDog/datadog-agent/pkg/util/option" +) + +type dependencies struct { + fx.In + + Status status.Component +} + +const headerText = "==========\nOTel Agent\n==========\n" + +// MakeCommand returns a `status` command to be used by agent binaries. +func MakeCommand(globalConfGetter func() *subcommands.GlobalParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "status", + Short: "Print the current status", + Long: ``, + RunE: func(*cobra.Command, []string) error { + globalParams := globalConfGetter() + return fxutil.OneShot( + runStatus, + fx.Supply(coreconfig.NewAgentParams(globalParams.CoreConfPath, coreconfig.WithExtraConfFiles(globalParams.ConfPaths))), + fx.Supply(option.None[secrets.Component]()), + fx.Supply(secrets.NewEnabledParams()), + fx.Supply(log.ForOneShot(globalParams.LoggerName, "off", true)), + coreconfig.Module(), + secretsimpl.Module(), + logfx.Module(), + fetchonlyimpl.Module(), + otelagentStatusfx.Module(), + ) + }, + } + + return cmd +} + +func runStatus(deps dependencies) error { + statusText, err := deps.Status.GetStatus() + if err != nil { + return err + } + _, err = os.Stdout.Write([]byte(headerText + statusText)) + if err != nil { + return err + } + return nil +} diff --git a/cmd/otel-agent/subcommands/status/command_test.go b/cmd/otel-agent/subcommands/status/command_test.go new file mode 100644 index 00000000000000..d8c3b6e3098cd6 --- /dev/null +++ b/cmd/otel-agent/subcommands/status/command_test.go @@ -0,0 +1,40 @@ +// 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 + +import ( + "os" + "path" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + + "github.com/DataDog/datadog-agent/cmd/otel-agent/subcommands" + "github.com/DataDog/datadog-agent/pkg/util/fxutil" +) + +func newGlobalParamsTest(t *testing.T) *subcommands.GlobalParams { + config := path.Join(t.TempDir(), "datadog.yaml") + err := os.WriteFile(config, []byte("hostname: test"), 0644) + require.NoError(t, err) + return &subcommands.GlobalParams{ + CoreConfPath: config, + ConfPaths: []string{"test_config.yaml"}, + } +} + +func TestStatusCommand(t *testing.T) { + globalConfGetter := func() *subcommands.GlobalParams { + return newGlobalParamsTest(t) + } + fxutil.TestOneShotSubcommand(t, + []*cobra.Command{MakeCommand(globalConfGetter)}, + []string{"status"}, + runStatus, + func() {}, + ) +} diff --git a/cmd/otel-agent/subcommands/status/test_config.yaml b/cmd/otel-agent/subcommands/status/test_config.yaml new file mode 100644 index 00000000000000..d24e4565e93cf3 --- /dev/null +++ b/cmd/otel-agent/subcommands/status/test_config.yaml @@ -0,0 +1,21 @@ +receivers: + otlp: + protocols: + http: + endpoint: "localhost:4318" + grpc: + endpoint: "localhost:4317" + +exporters: + datadog: + api: + key: "abc" + +service: + pipelines: + traces: + receivers: [otlp] + exporters: [datadog] + telemetry: + metrics: + address: 127.0.0.1:8888 diff --git a/comp/otelcol/status/def/component.go b/comp/otelcol/status/def/component.go index 0ea47afbfe9922..27197534d3605c 100644 --- a/comp/otelcol/status/def/component.go +++ b/comp/otelcol/status/def/component.go @@ -10,4 +10,6 @@ package status // Component is the status interface. type Component interface { + // GetStatus returns the OTel Agent status in string form + GetStatus() (string, error) } diff --git a/comp/otelcol/status/impl/status.go b/comp/otelcol/status/impl/status.go index b0bc3c8cb42988..5b174114096d1c 100644 --- a/comp/otelcol/status/impl/status.go +++ b/comp/otelcol/status/impl/status.go @@ -7,6 +7,7 @@ package statusimpl import ( + "bytes" "embed" "encoding/json" "fmt" @@ -14,6 +15,7 @@ import ( "io" "net/http" + "github.com/DataDog/datadog-agent/comp/api/authtoken" "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" @@ -27,7 +29,8 @@ var templatesFS embed.FS // Requires defines the dependencies of the status component. type Requires struct { - Config config.Component + Config config.Component + Authtoken authtoken.Component } // Provides contains components provided by status constructor. @@ -39,6 +42,7 @@ type Provides struct { type statusProvider struct { Config config.Component client *http.Client + authToken authtoken.Component receiverStatus map[string]interface{} exporterStatus map[string]interface{} } @@ -65,8 +69,9 @@ type prometheusRuntimeConfig struct { // NewComponent creates a new status component. func NewComponent(reqs Requires) Provides { comp := statusProvider{ - Config: reqs.Config, - client: apiutil.GetClient(false), + Config: reqs.Config, + client: apiutil.GetClient(false), + authToken: reqs.Authtoken, receiverStatus: map[string]interface{}{ "spans": 0.0, "metrics": 0.0, @@ -100,6 +105,16 @@ func (s statusProvider) Section() string { return "OTel Agent" } +// GetStatus returns the OTel Agent status in string form +func (s statusProvider) GetStatus() (string, error) { + buf := new(bytes.Buffer) + err := s.Text(false, buf) + if err != nil { + return "", err + } + return buf.String(), nil +} + func (s statusProvider) getStatusInfo() map[string]interface{} { statusInfo := make(map[string]interface{}) @@ -172,7 +187,11 @@ func (s statusProvider) populatePrometheusStatus(prometheusURL string) error { func (s statusProvider) populateStatus() map[string]interface{} { extensionURL := s.Config.GetString("otelcollector.extension_url") - resp, err := apiutil.DoGet(s.client, extensionURL, apiutil.CloseConnection) + options := apiutil.ReqOptions{ + Conn: apiutil.CloseConnection, + Authtoken: s.authToken.Get(), + } + resp, err := apiutil.DoGetWithOptions(s.client, extensionURL, &options) if err != nil { return map[string]interface{}{ "url": extensionURL, diff --git a/comp/otelcol/status/impl/status_test.go b/comp/otelcol/status/impl/status_test.go index f11cefd7eed973..e55f3a71fbb324 100644 --- a/comp/otelcol/status/impl/status_test.go +++ b/comp/otelcol/status/impl/status_test.go @@ -11,6 +11,8 @@ import ( "github.com/stretchr/testify/assert" + "github.com/DataDog/datadog-agent/comp/api/authtoken" + "github.com/DataDog/datadog-agent/comp/api/authtoken/fetchonlyimpl" "github.com/DataDog/datadog-agent/comp/core/config" "github.com/DataDog/datadog-agent/comp/core/status" ) @@ -47,7 +49,8 @@ func TestStatusOut(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { provides := NewComponent(Requires{ - Config: config.NewMock(t), + Config: config.NewMock(t), + Authtoken: authtoken.Component(&fetchonlyimpl.MockFetchOnly{}), }) headerProvider := provides.StatusProvider.Provider test.assertFunc(t, headerProvider) diff --git a/test/new-e2e/tests/otel/otel-agent/minimal_test.go b/test/new-e2e/tests/otel/otel-agent/minimal_test.go index ee33ec9d6cbacc..1e0c5bc8630cb0 100644 --- a/test/new-e2e/tests/otel/otel-agent/minimal_test.go +++ b/test/new-e2e/tests/otel/otel-agent/minimal_test.go @@ -92,3 +92,11 @@ func (s *minimalTestSuite) TestOTelFlareFiles() { func (s *minimalTestSuite) TestOTelRemoteConfigPayload() { utils.TestOTelRemoteConfigPayload(s, minimalProvidedConfig, minimalFullConfig) } + +func (s *minimalTestSuite) TestCoreAgentStatus() { + utils.TestCoreAgentStatusCmd(s) +} + +func (s *minimalTestSuite) TestOTelAgentStatus() { + utils.TestOTelAgentStatusCmd(s) +} diff --git a/test/new-e2e/tests/otel/utils/config_utils.go b/test/new-e2e/tests/otel/utils/config_utils.go index 3d10d5267bc2e1..c054a966e31b10 100644 --- a/test/new-e2e/tests/otel/utils/config_utils.go +++ b/test/new-e2e/tests/otel/utils/config_utils.go @@ -227,3 +227,43 @@ func validateConfigs(t *testing.T, expectedCfg string, actualCfg string) { assert.YAMLEq(t, expectedCfg, actualCfg) } + +// TestCoreAgentStatusCmd tests the core agent status command contains the OTel Agent status as expected +func TestCoreAgentStatusCmd(s OTelTestSuite) { + err := s.Env().FakeIntake.Client().FlushServerAndResetAggregators() + require.NoError(s.T(), err) + agent := getAgentPod(s) + + s.T().Log("Calling status command in core agent") + stdout, stderr, err := s.Env().KubernetesCluster.KubernetesClient.PodExec("datadog", agent.Name, "agent", []string{"agent", "status", "otel agent"}) + require.NoError(s.T(), err, "Failed to execute config") + require.Empty(s.T(), stderr) + validateStatus(s.T(), stdout) +} + +// TestOTelAgentStatusCmd tests the OTel Agent status subcommand returns as expected +func TestOTelAgentStatusCmd(s OTelTestSuite) { + err := s.Env().FakeIntake.Client().FlushServerAndResetAggregators() + require.NoError(s.T(), err) + agent := getAgentPod(s) + + s.T().Log("Calling status command in otel agent") + stdout, stderr, err := s.Env().KubernetesCluster.KubernetesClient.PodExec("datadog", agent.Name, "otel-agent", []string{"otel-agent", "status"}) + require.NoError(s.T(), err, "Failed to execute config") + require.Empty(s.T(), stderr) + validateStatus(s.T(), stdout) +} + +func validateStatus(t *testing.T, status string) { + require.NotNil(t, status) + require.Contains(t, status, "OTel Agent") + require.Contains(t, status, "Status: Running") + require.Contains(t, status, "Agent Version:") + require.Contains(t, status, "Collector Version:") + require.Contains(t, status, "Spans Accepted:") + require.Contains(t, status, "Metric Points Accepted:") + require.Contains(t, status, "Log Records Accepted:") + require.Contains(t, status, "Spans Sent:") + require.Contains(t, status, "Metric Points Sent:") + require.Contains(t, status, "Log Records Sent:") +}