Skip to content

Commit

Permalink
Add unit tests using mocked cloud foundry hubs
Browse files Browse the repository at this point in the history
  • Loading branch information
jsoriano committed Nov 20, 2020
1 parent 5512aa8 commit 52f72fc
Show file tree
Hide file tree
Showing 11 changed files with 547 additions and 9 deletions.
2 changes: 1 addition & 1 deletion x-pack/libbeat/common/cloudfoundry/dopplerconsumer.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func (c *DopplerConsumer) firehose(cb func(evt Event), filter consumer.EnvelopeF
if !filterFn(env) {
continue
}
event := envelopeToEvent(env)
event := EnvelopeToEvent(env)
if event == nil {
c.log.Debugf("Envelope couldn't be converted to event: %+v", env)
continue
Expand Down
2 changes: 1 addition & 1 deletion x-pack/libbeat/common/cloudfoundry/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ func newEventError(env *events.Envelope) *EventError {
}
}

func envelopeToEvent(env *events.Envelope) Event {
func EnvelopeToEvent(env *events.Envelope) Event {
switch *env.EventType {
case events.Envelope_HttpStartStop:
return newEventHttpAccess(env)
Expand Down
2 changes: 1 addition & 1 deletion x-pack/libbeat/common/cloudfoundry/rlplistener.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func (c *RlpListener) Start(ctx context.Context) {
for i := range envelopes {
v1s := conversion.ToV1(envelopes[i])
for _, v := range v1s {
evt := envelopeToEvent(v)
evt := EnvelopeToEvent(v)
if evt.EventType() == EventTypeHttpAccess && c.callbacks.HttpAccess != nil {
c.callbacks.HttpAccess(evt.(*EventHttpAccess))
} else if evt.EventType() == EventTypeLog && c.callbacks.Log != nil {
Expand Down
13 changes: 12 additions & 1 deletion x-pack/metricbeat/module/cloudfoundry/cloudfoundry.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,30 @@ func init() {
}

type Module interface {
mb.Module
RunCounterReporter(mb.PushReporterV2)
RunContainerReporter(mb.PushReporterV2)
RunValueReporter(mb.PushReporterV2)
}

func newModule(base mb.BaseModule) (mb.Module, error) {
factory := func(cfg *cfcommon.Config, name string, log *logp.Logger) CloudfoundryHub {
return &HubAdapter{cfcommon.NewHub(cfg, name, log)}
}
return NewModuleWithHubFactory(base, factory)
}

type hubFactory func(cfg *cfcommon.Config, name string, log *logp.Logger) CloudfoundryHub

// NewModuleWithHubFactory initializes a module with a hub created with a hub factory
func NewModuleWithHubFactory(base mb.BaseModule, hubFactory hubFactory) (mb.Module, error) {
var cfg cfcommon.Config
if err := base.UnpackConfig(&cfg); err != nil {
return nil, err
}

log := logp.NewLogger("cloudfoundry")
hub := cfcommon.NewHub(&cfg, "metricbeat", log)
hub := hubFactory(&cfg, "metricbeat", log)

switch cfg.Version {
case cfcommon.ConsumerVersionV1:
Expand Down
163 changes: 163 additions & 0 deletions x-pack/metricbeat/module/cloudfoundry/container/container_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

// +build !integration

package container

import (
"math"
"testing"
"time"

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

"github.com/cloudfoundry/sonde-go/events"

"github.com/elastic/beats/v7/libbeat/common"
"github.com/elastic/beats/v7/libbeat/logp"
"github.com/elastic/beats/v7/metricbeat/mb"
"github.com/elastic/beats/v7/metricbeat/mb/parse"
mbtest "github.com/elastic/beats/v7/metricbeat/mb/testing"
"github.com/elastic/beats/v7/x-pack/metricbeat/module/cloudfoundry/mtest"
)

func init() {
if err := mb.Registry.AddModule("cloudfoundrytest", mtest.NewModuleMock); err != nil {
panic(err)
}
mb.Registry.MustAddMetricSet("cloudfoundrytest", "test", newTestMetricSet,
mb.WithHostParser(parse.EmptyHostParser),
mb.DefaultMetricSet(),
)
}

func newTestMetricSet(base mb.BaseMetricSet) (mb.MetricSet, error) {
return New(base)
}

func TestMetricSet(t *testing.T) {
logp.TestingSetup(logp.WithSelectors("cloudfoundry"))

config := map[string]interface{}{
"module": "cloudfoundrytest",
"client_id": "dummy",
"client_secret": "dummy",
"api_address": "dummy",
"shard_id": "dummy",
}

ms := mbtest.NewPushMetricSetV2(t, config)
hub := ms.Module().(*mtest.ModuleMock).Hub

go func() {
hub.SendEnvelope(containerMetricsEnvelope(containerMetrics{app: "1234", memory: 1024, cpupct: 12.34}))
}()

events := mbtest.RunPushMetricSetV2(10*time.Second, 1, ms)
require.NotEmpty(t, events)

expectedFields := common.MapStr{
"cloudfoundry.app.id": "1234",
"cloudfoundry.container.cpu.pct": float64(12.34),
"cloudfoundry.container.disk.bytes": uint64(0),
"cloudfoundry.container.disk.quota.bytes": uint64(0),
"cloudfoundry.container.instance_index": int32(0),
"cloudfoundry.container.memory.bytes": uint64(1024),
"cloudfoundry.container.memory.quota.bytes": uint64(0),
"cloudfoundry.envelope.deployment": "test",
"cloudfoundry.envelope.index": "index",
"cloudfoundry.envelope.ip": "127.0.0.1",
"cloudfoundry.envelope.job": "test",
"cloudfoundry.envelope.origin": "test",
"cloudfoundry.type": "container",
}
require.Equal(t, expectedFields, events[0].RootFields.Flatten())
}

func TestMetricValuesAreNumbers(t *testing.T) {
logp.TestingSetup(logp.WithSelectors("cloudfoundry"))

config := map[string]interface{}{
"module": "cloudfoundrytest",
"client_id": "dummy",
"client_secret": "dummy",
"api_address": "dummy",
"shard_id": "dummy",
}

ms := mbtest.NewPushMetricSetV2(t, config)
hub := ms.Module().(*mtest.ModuleMock).Hub

go func() {
hub.SendEnvelope(containerMetricsEnvelope(containerMetrics{app: "0000", memory: 1024, cpupct: math.NaN()}))
hub.SendEnvelope(containerMetricsEnvelope(containerMetrics{app: "1234", memory: 1024, cpupct: 12.34}))
}()

events := mbtest.RunPushMetricSetV2(10*time.Second, 2, ms)
require.NotEmpty(t, events)

for _, e := range events {
memory, err := e.RootFields.GetValue("cloudfoundry.container.memory.bytes")
if assert.NoError(t, err, "checking memory") {
assert.Equal(t, uint64(1024), memory.(uint64))
}

app, err := e.RootFields.GetValue("cloudfoundry.app.id")
require.NoError(t, err, "getting app id")

cpuPctKey := "cloudfoundry.container.cpu.pct"
switch app {
case "0000":
_, err := e.RootFields.GetValue(cpuPctKey)
require.Error(t, err, "non-numeric metric shouldn't be there")
case "1234":
v, err := e.RootFields.GetValue(cpuPctKey)
if assert.NoError(t, err, "checking cpu pct") {
assert.Equal(t, 12.34, v.(float64))
}
default:
t.Errorf("unexpected app: %s", app)
}
}
}

type containerMetrics struct {
app string
instance int32
cpupct float64
memory uint64
disk uint64
memoryQuota uint64
diskQuota uint64
}

func containerMetricsEnvelope(metrics containerMetrics) *events.Envelope {
eventType := events.Envelope_ContainerMetric
origin := "test"
deployment := "test"
job := "test"
ip := "127.0.0.1"
index := "index"
timestamp := time.Now().Unix()
return &events.Envelope{
EventType: &eventType,
Timestamp: &timestamp,
Origin: &origin,
Deployment: &deployment,
Job: &job,
Ip: &ip,
Index: &index,
ContainerMetric: &events.ContainerMetric{
ApplicationId: &metrics.app,
InstanceIndex: &metrics.instance,
CpuPercentage: &metrics.cpupct,
MemoryBytes: &metrics.memory,
DiskBytes: &metrics.disk,
MemoryBytesQuota: &metrics.memoryQuota,
DiskBytesQuota: &metrics.diskQuota,
},
}
}
96 changes: 96 additions & 0 deletions x-pack/metricbeat/module/cloudfoundry/counter/counter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

// +build !integration

package counter

import (
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/cloudfoundry/sonde-go/events"

"github.com/elastic/beats/v7/libbeat/common"
"github.com/elastic/beats/v7/libbeat/logp"
"github.com/elastic/beats/v7/metricbeat/mb"
"github.com/elastic/beats/v7/metricbeat/mb/parse"
mbtest "github.com/elastic/beats/v7/metricbeat/mb/testing"
"github.com/elastic/beats/v7/x-pack/metricbeat/module/cloudfoundry/mtest"
)

func init() {
if err := mb.Registry.AddModule("cloudfoundrytest", mtest.NewModuleMock); err != nil {
panic(err)
}
mb.Registry.MustAddMetricSet("cloudfoundrytest", "test", newTestMetricSet,
mb.WithHostParser(parse.EmptyHostParser),
mb.DefaultMetricSet(),
)
}

func newTestMetricSet(base mb.BaseMetricSet) (mb.MetricSet, error) {
return New(base)
}

func TestMetricSet(t *testing.T) {
logp.TestingSetup(logp.WithSelectors("cloudfoundry"))

config := map[string]interface{}{
"module": "cloudfoundrytest",
"client_id": "dummy",
"client_secret": "dummy",
"api_address": "dummy",
"shard_id": "dummy",
}

ms := mbtest.NewPushMetricSetV2(t, config)
hub := ms.Module().(*mtest.ModuleMock).Hub

go func() {
hub.SendEnvelope(counterMetricEnvelope("requests", 1234, 123))
}()

events := mbtest.RunPushMetricSetV2(10*time.Second, 1, ms)
require.NotEmpty(t, events)

expectedFields := common.MapStr{
"cloudfoundry.counter.delta": uint64(123),
"cloudfoundry.counter.name": "requests",
"cloudfoundry.counter.total": uint64(1234),
"cloudfoundry.envelope.deployment": "test",
"cloudfoundry.envelope.index": "index",
"cloudfoundry.envelope.ip": "127.0.0.1",
"cloudfoundry.envelope.job": "test",
"cloudfoundry.envelope.origin": "test",
"cloudfoundry.type": "counter",
}
require.Equal(t, expectedFields, events[0].RootFields.Flatten())
}

func counterMetricEnvelope(name string, total uint64, delta uint64) *events.Envelope {
eventType := events.Envelope_CounterEvent
origin := "test"
deployment := "test"
job := "test"
ip := "127.0.0.1"
index := "index"
timestamp := time.Now().Unix()
return &events.Envelope{
EventType: &eventType,
Timestamp: &timestamp,
Origin: &origin,
Deployment: &deployment,
Job: &job,
Ip: &ip,
Index: &index,
CounterEvent: &events.CounterEvent{
Name: &name,
Total: &total,
Delta: &delta,
},
}
}
43 changes: 43 additions & 0 deletions x-pack/metricbeat/module/cloudfoundry/hub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package cloudfoundry

import (
"context"

cfcommon "github.com/elastic/beats/v7/x-pack/libbeat/common/cloudfoundry"
)

// DopplerConsumer is the interface that a Doppler Consumer must implement for the Cloud Foundry module.
type DopplerConsumer interface {
Run()
Stop()
}

// RlpListener is the interface that a RLP listener must implement for the Cloud Foundry module.
type RlpListener interface {
Start(context.Context)
Stop()
}

// CloudfoundryHub is the interface that a Hub must implement for the Cloud Foundry module.
type CloudfoundryHub interface {
DopplerConsumer(cfcommon.DopplerCallbacks) (DopplerConsumer, error)
RlpListener(cfcommon.RlpListenerCallbacks) (RlpListener, error)
}

// HubAdapter adapt a cloudfoundry Hub to the hub expected by the metricbeat module.
// This adaptation is needed to return different but compatible types, so the Hub can be mocked.
type HubAdapter struct {
hub *cfcommon.Hub
}

func (h *HubAdapter) DopplerConsumer(cbs cfcommon.DopplerCallbacks) (DopplerConsumer, error) {
return h.hub.DopplerConsumer(cbs)
}

func (h *HubAdapter) RlpListener(cbs cfcommon.RlpListenerCallbacks) (RlpListener, error) {
return h.hub.RlpListener(cbs)
}
Loading

0 comments on commit 52f72fc

Please sign in to comment.