Skip to content

Commit

Permalink
Add agent stats handler. (#920)
Browse files Browse the repository at this point in the history
  • Loading branch information
jefchien authored Oct 24, 2023
1 parent 0e44c4d commit b99bd36
Show file tree
Hide file tree
Showing 23 changed files with 934 additions and 17 deletions.
9 changes: 7 additions & 2 deletions extension/agenthealth/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@

package agenthealth

import "go.opentelemetry.io/collector/component"
import (
"go.opentelemetry.io/collector/component"

"github.com/aws/amazon-cloudwatch-agent/extension/agenthealth/handler/stats/agent"
)

type Config struct {
IsUsageDataEnabled bool `mapstructure:"is_usage_data_enabled"`
IsUsageDataEnabled bool `mapstructure:"is_usage_data_enabled"`
Stats agent.StatsConfig `mapstructure:"stats"`
}

var _ component.Config = (*Config)(nil)
8 changes: 7 additions & 1 deletion extension/agenthealth/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/confmap/confmaptest"

"github.com/aws/amazon-cloudwatch-agent/extension/agenthealth/handler/stats/agent"
)

func TestLoadConfig(t *testing.T) {
Expand All @@ -24,7 +26,11 @@ func TestLoadConfig(t *testing.T) {
},
{
id: component.NewIDWithName(TypeStr, "1"),
want: &Config{IsUsageDataEnabled: false},
want: &Config{IsUsageDataEnabled: false, Stats: agent.StatsConfig{Operations: []string{agent.AllowAllOperations}}},
},
{
id: component.NewIDWithName(TypeStr, "2"),
want: &Config{IsUsageDataEnabled: true, Stats: agent.StatsConfig{Operations: []string{"ListBuckets"}}},
},
}
for _, testCase := range testCases {
Expand Down
6 changes: 6 additions & 0 deletions extension/agenthealth/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"go.opentelemetry.io/collector/component"
"go.uber.org/zap"

"github.com/aws/amazon-cloudwatch-agent/extension/agenthealth/handler/stats"
"github.com/aws/amazon-cloudwatch-agent/extension/agenthealth/handler/useragent"
)

Expand All @@ -23,6 +24,11 @@ var _ awsmiddleware.Extension = (*agentHealth)(nil)
func (ah *agentHealth) Handlers() ([]awsmiddleware.RequestHandler, []awsmiddleware.ResponseHandler) {
var responseHandlers []awsmiddleware.ResponseHandler
requestHandlers := []awsmiddleware.RequestHandler{useragent.NewHandler(ah.cfg.IsUsageDataEnabled)}
if ah.cfg.IsUsageDataEnabled {
req, res := stats.NewHandlers(ah.logger, ah.cfg.Stats)
requestHandlers = append(requestHandlers, req...)
responseHandlers = append(responseHandlers, res...)
}
return requestHandlers, responseHandlers
}

Expand Down
16 changes: 9 additions & 7 deletions extension/agenthealth/extension_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ func TestExtension(t *testing.T) {
assert.NoError(t, err)
assert.NotNil(t, extension)
assert.NoError(t, extension.Start(ctx, componenttest.NewNopHost()))
requests, responses := extension.Handlers()
assert.Len(t, requests, 1)
assert.Len(t, responses, 0)
requestHandlers, responseHandlers := extension.Handlers()
// user agent, client stats, stats
assert.Len(t, requestHandlers, 3)
// client stats
assert.Len(t, responseHandlers, 1)
extension.cfg.IsUsageDataEnabled = false
extension.Handlers()
requests, responses = extension.Handlers()
assert.Len(t, requests, 1)
assert.Len(t, responses, 0)
requestHandlers, responseHandlers = extension.Handlers()
// user agent
assert.Len(t, requestHandlers, 1)
assert.Len(t, responseHandlers, 0)
assert.NoError(t, extension.Shutdown(ctx))
}
5 changes: 5 additions & 0 deletions extension/agenthealth/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/extension"

"github.com/aws/amazon-cloudwatch-agent/extension/agenthealth/handler/stats/agent"
)

const (
Expand All @@ -26,6 +28,9 @@ func NewFactory() extension.Factory {
func createDefaultConfig() component.Config {
return &Config{
IsUsageDataEnabled: true,
Stats: agent.StatsConfig{
Operations: []string{agent.AllowAllOperations},
},
}
}

Expand Down
4 changes: 3 additions & 1 deletion extension/agenthealth/factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import (
"github.com/stretchr/testify/assert"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/extension/extensiontest"

"github.com/aws/amazon-cloudwatch-agent/extension/agenthealth/handler/stats/agent"
)

func TestCreateDefaultConfig(t *testing.T) {
cfg := NewFactory().CreateDefaultConfig()
assert.Equal(t, &Config{IsUsageDataEnabled: true}, cfg)
assert.Equal(t, &Config{IsUsageDataEnabled: true, Stats: agent.StatsConfig{Operations: []string{agent.AllowAllOperations}}}, cfg)
assert.NoError(t, componenttest.CheckConfigStruct(cfg))
}

Expand Down
102 changes: 102 additions & 0 deletions extension/agenthealth/handler/stats/agent/agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT

package agent

import (
"encoding/json"
"strings"

"github.com/aws/amazon-cloudwatch-agent/internal/util/collections"
)

const (
AllowAllOperations = "*"
)

type Stats struct {
CpuPercent *float64 `json:"cpu,omitempty"`
MemoryBytes *uint64 `json:"mem,omitempty"`
FileDescriptorCount *int32 `json:"fd,omitempty"`
ThreadCount *int32 `json:"th,omitempty"`
LatencyMillis *int64 `json:"lat,omitempty"`
PayloadBytes *int `json:"load,omitempty"`
StatusCode *int `json:"code,omitempty"`
SharedConfigFallback *int `json:"scfb,omitempty"`
ImdsFallbackSucceed *int `json:"ifs,omitempty"`
AppSignals *int `json:"as,omitempty"`
EnhancedContainerInsights *int `json:"eci,omitempty"`
}

// Merge the other Stats into the current. If the field is not nil,
// then it'll overwrite the existing one.
func (s *Stats) Merge(other Stats) {
if other.CpuPercent != nil {
s.CpuPercent = other.CpuPercent
}
if other.MemoryBytes != nil {
s.MemoryBytes = other.MemoryBytes
}
if other.FileDescriptorCount != nil {
s.FileDescriptorCount = other.FileDescriptorCount
}
if other.ThreadCount != nil {
s.ThreadCount = other.ThreadCount
}
if other.LatencyMillis != nil {
s.LatencyMillis = other.LatencyMillis
}
if other.PayloadBytes != nil {
s.PayloadBytes = other.PayloadBytes
}
if other.StatusCode != nil {
s.StatusCode = other.StatusCode
}
if other.SharedConfigFallback != nil {
s.SharedConfigFallback = other.SharedConfigFallback
}
if other.ImdsFallbackSucceed != nil {
s.ImdsFallbackSucceed = other.ImdsFallbackSucceed
}
if other.AppSignals != nil {
s.AppSignals = other.AppSignals
}
if other.EnhancedContainerInsights != nil {
s.EnhancedContainerInsights = other.EnhancedContainerInsights
}
}

func (s *Stats) Marshal() (string, error) {
raw, err := json.Marshal(s)
if err != nil {
return "", err
}
content := strings.TrimPrefix(string(raw), "{")
return strings.TrimSuffix(content, "}"), nil
}

type StatsProvider interface {
Stats(operation string) Stats
}

type OperationsFilter struct {
operations collections.Set[string]
allowAll bool
}

func (of OperationsFilter) IsAllowed(operationName string) bool {
return of.allowAll || of.operations.Contains(operationName)
}

func NewOperationsFilter(operations ...string) OperationsFilter {
allowed := collections.NewSet[string](operations...)
return OperationsFilter{
operations: allowed,
allowAll: allowed.Contains(AllowAllOperations),
}
}

type StatsConfig struct {
// Operations are the allowed operation names to gather stats for.
Operations []string `mapstructure:"operations,omitempty"`
}
106 changes: 106 additions & 0 deletions extension/agenthealth/handler/stats/agent/agent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT

package agent

import (
"testing"

"github.com/aws/aws-sdk-go/aws"
"github.com/stretchr/testify/assert"
)

func TestMerge(t *testing.T) {
stats := &Stats{CpuPercent: aws.Float64(1.2)}
assert.EqualValues(t, 1.2, *stats.CpuPercent)
assert.Nil(t, stats.MemoryBytes)
stats.Merge(Stats{
CpuPercent: aws.Float64(1.3),
MemoryBytes: aws.Uint64(123),
})
assert.EqualValues(t, 1.3, *stats.CpuPercent)
assert.EqualValues(t, 123, *stats.MemoryBytes)
stats.Merge(Stats{
FileDescriptorCount: aws.Int32(456),
ThreadCount: aws.Int32(789),
LatencyMillis: aws.Int64(1234),
PayloadBytes: aws.Int(5678),
StatusCode: aws.Int(200),
ImdsFallbackSucceed: aws.Int(1),
SharedConfigFallback: aws.Int(1),
AppSignals: aws.Int(1),
EnhancedContainerInsights: aws.Int(1),
})
assert.EqualValues(t, 1.3, *stats.CpuPercent)
assert.EqualValues(t, 123, *stats.MemoryBytes)
assert.EqualValues(t, 456, *stats.FileDescriptorCount)
assert.EqualValues(t, 789, *stats.ThreadCount)
assert.EqualValues(t, 1234, *stats.LatencyMillis)
assert.EqualValues(t, 5678, *stats.PayloadBytes)
assert.EqualValues(t, 200, *stats.StatusCode)
assert.EqualValues(t, 1, *stats.ImdsFallbackSucceed)
assert.EqualValues(t, 1, *stats.SharedConfigFallback)
assert.EqualValues(t, 1, *stats.AppSignals)
assert.EqualValues(t, 1, *stats.EnhancedContainerInsights)
}

func TestMarshal(t *testing.T) {
testCases := map[string]struct {
stats *Stats
want string
}{
"WithEmpty": {
stats: &Stats{},
want: "",
},
"WithPartial": {
stats: &Stats{
CpuPercent: aws.Float64(1.2),
MemoryBytes: aws.Uint64(123),
ThreadCount: aws.Int32(789),
PayloadBytes: aws.Int(5678),
},
want: `"cpu":1.2,"mem":123,"th":789,"load":5678`,
},
"WithFull": {
stats: &Stats{
CpuPercent: aws.Float64(1.2),
MemoryBytes: aws.Uint64(123),
FileDescriptorCount: aws.Int32(456),
ThreadCount: aws.Int32(789),
LatencyMillis: aws.Int64(1234),
PayloadBytes: aws.Int(5678),
StatusCode: aws.Int(200),
ImdsFallbackSucceed: aws.Int(1),
},
want: `"cpu":1.2,"mem":123,"fd":456,"th":789,"lat":1234,"load":5678,"code":200,"ifs":1`,
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
got, err := testCase.stats.Marshal()
assert.NoError(t, err)
assert.Equal(t, testCase.want, got)
})
}
}

func TestOperationFilter(t *testing.T) {
testCases := map[string]struct {
allowedOperations []string
testOperations []string
want []bool
}{
"WithNoneAllowed": {allowedOperations: nil, testOperations: []string{"nothing", "is", "allowed"}, want: []bool{false, false, false}},
"WithSomeAllowed": {allowedOperations: []string{"are"}, testOperations: []string{"some", "are", "allowed"}, want: []bool{false, true, false}},
"WithAllAllowed": {allowedOperations: []string{"*"}, testOperations: []string{"all", "are", "allowed"}, want: []bool{true, true, true}},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
filter := NewOperationsFilter(testCase.allowedOperations...)
for index, testOperation := range testCase.testOperations {
assert.Equal(t, testCase.want[index], filter.IsAllowed(testOperation))
}
})
}
}
Loading

0 comments on commit b99bd36

Please sign in to comment.