Skip to content

Commit

Permalink
Add functional test
Browse files Browse the repository at this point in the history
Problem:
Ensure the exporter library correctly sends telemetry data via OTel

Solution:
Add a functional test:
- Exporter sends telemetry data to an OTel collector
- Ensure the collector successfully receives the data by looking at
its logs
- Include the test into GitHub pipeline.

CLOSES #7
  • Loading branch information
pleshakov committed Feb 26, 2024
1 parent 2ec8c67 commit 84dcb16
Show file tree
Hide file tree
Showing 9 changed files with 424 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ updates:
directory: /
schedule:
interval: daily

- package-ecosystem: docker
directory: /tests
schedule:
interval: daily
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,18 @@ jobs:
name: cover-${{ github.run_id }}.html
path: ${{ github.workspace }}/cmd-cover.html
if: always()

functional-tests:
name: Unit Tests
runs-on: ubuntu-22.04
steps:
- name: Checkout Repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

- name: Setup Golang Environment
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
with:
go-version: stable

- name: Run Functional Tests
run: make functional-test
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ help: Makefile ## Display this help

.PHONY: unit-test
unit-test:
go test ./... -race -coverprofile cmd-cover.out
go test pkg/... -race -coverprofile cmd-cover.out
go tool cover -html=cmd-cover.out -o cmd-cover.html

.PHONY: clean-go-cache
Expand Down Expand Up @@ -36,3 +36,8 @@ dev-all: deps fmt vet lint unit-test ## Run all the development checks
.PHONY: generate
generate: ## Run go generate
go generate ./...

.PHONY: functional-test
functional-test: ## Run functional tests
go run github.com/onsi/ginkgo/v2/ginkgo --randomize-all --randomize-suites --race --keep-going --fail-on-pending --trace -r tests

36 changes: 36 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,59 @@ require (
github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1
github.com/onsi/ginkgo/v2 v2.15.0
github.com/onsi/gomega v1.31.1
github.com/testcontainers/testcontainers-go v0.28.0
go.opentelemetry.io/otel v1.23.1
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.1
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.23.1
go.opentelemetry.io/otel/sdk v1.23.1
)

require (
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Microsoft/hcsshim v0.11.4 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/containerd/containerd v1.7.12 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/cpuguy83/dockercfg v0.3.1 // indirect
github.com/distribution/reference v0.5.0 // indirect
github.com/docker/docker v25.0.3+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
github.com/klauspost/compress v1.16.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect
go.opentelemetry.io/otel/metric v1.23.1 // indirect
go.opentelemetry.io/otel/trace v1.23.1 // indirect
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect
Expand Down
126 changes: 126 additions & 0 deletions go.sum

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions tests/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# syntax=docker/dockerfile:1.6
# this is here so we can grab the latest version of the collector and have dependabot keep it up to date
FROM otel/opentelemetry-collector-contrib:0.88.0
30 changes: 30 additions & 0 deletions tests/collector.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
exporters:
debug:
verbosity: detailed
logging: {}
extensions:
health_check: {}
memory_ballast:
size_in_percentage: 40
processors:
batch: {}
memory_limiter:
check_interval: 5s
limit_percentage: 80
spike_limit_percentage: 25
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
# http:
# endpoint: 0.0.0.0:4318
service:
extensions:
- health_check
pipelines:
traces:
exporters:
- debug
receivers:
- otlp
190 changes: 190 additions & 0 deletions tests/exporter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package tests

import (
"bufio"
"context"
"fmt"
"io"
"log/slog"
"os"
"strings"
"sync"

"github.com/go-logr/logr"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"

"github.com/nginxinc/telemetry-exporter/pkg/telemetry"
)

type telemetryData struct {
ResourceCount int
}

func (d *telemetryData) Attributes() []attribute.KeyValue {
return []attribute.KeyValue{
attribute.Int("resourceCount", d.ResourceCount),
}
}

type matchingLogConsumer struct {
expectedSubstrings map[string]struct{}
sync sync.Mutex
}

func (c *matchingLogConsumer) Accept(log testcontainers.Log) {
c.sync.Lock()
defer c.sync.Unlock()

line := string(log.Content)

for k := range c.expectedSubstrings {
if strings.Contains(line, k) {
delete(c.expectedSubstrings, k)
break
}
}
}

func (c *matchingLogConsumer) setExpectedSubstrings(substrings []string) {
c.sync.Lock()
defer c.sync.Unlock()

c.expectedSubstrings = make(map[string]struct{}, len(substrings))
for _, s := range substrings {
c.expectedSubstrings[s] = struct{}{}
}
}

func (c *matchingLogConsumer) unmatchedCount() int {
c.sync.Lock()
defer c.sync.Unlock()
return len(c.expectedSubstrings)
}

func getCollectorImageFromDockerfile() (string, error) {
dockerFile, err := os.Open("Dockerfile")
if err != nil {
return "", fmt.Errorf("failed to open Dockerfile: %w", err)
}
defer dockerFile.Close()

reader := bufio.NewReader(dockerFile)

for {
line, err := reader.ReadString('\n')
if err == io.EOF {
return "", fmt.Errorf("FROM not found in Dockerfile")
}
if err != nil {
return "", fmt.Errorf("failed to read Dockerfile: %w", err)
}

if !strings.HasPrefix(line, "FROM ") {
continue
}

return strings.TrimSpace(strings.TrimPrefix(line, "FROM ")), nil
}
}

var _ = Describe("Exporter", func() {
var (
lc *matchingLogConsumer
exporter *telemetry.Exporter
collector testcontainers.Container
ctx context.Context
)

BeforeEach(func() {
ctx := context.Background()

// Run the collector container

image, err := getCollectorImageFromDockerfile()
Expect(err).ToNot(HaveOccurred())

const collectorCfgName = "collector.yaml"

lc = &matchingLogConsumer{}

req := testcontainers.ContainerRequest{
Image: image,
Files: []testcontainers.ContainerFile{
{
HostFilePath: "./" + collectorCfgName,
ContainerFilePath: "/" + collectorCfgName,
FileMode: 0o444,
},
},
ExposedPorts: []string{"4317/tcp"},
WaitingFor: wait.ForLog("Everything is ready. Begin running and processing data."),
LogConsumerCfg: &testcontainers.LogConsumerConfig{
Consumers: []testcontainers.LogConsumer{lc},
},
Cmd: []string{"--config=/" + collectorCfgName},
}

collector, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
Expect(err).ToNot(HaveOccurred())

// Create the exporter

ip, err := collector.Host(ctx)
Expect(err).ToNot(HaveOccurred())

port, err := collector.MappedPort(ctx, "4317")
Expect(err).ToNot(HaveOccurred())

endpoint := fmt.Sprintf("%s:%s", ip, port.Port())

logger := logr.FromSlogHandler(slog.Default().Handler())

errorHandler := telemetry.NewErrorHandler()

exporter, err = telemetry.NewExporter(
telemetry.ExporterConfig{
SpanProvider: telemetry.CreateOTLPSpanProvider(
otlptracegrpc.WithEndpoint(endpoint),
otlptracegrpc.WithInsecure(),
),
},
telemetry.WithGlobalOTelLogger(logger),
telemetry.WithGlobalOTelErrorHandler(errorHandler),
)
Expect(err).ToNot(HaveOccurred())
})

AfterEach(func() {
if collector != nil {
err := collector.Terminate(ctx)
Expect(err).ToNot(HaveOccurred())
}
if exporter != nil {
err := exporter.Shutdown(ctx)
Expect(err).ToNot(HaveOccurred())
}
})

It("exports data successfully", func() {
lc.setExpectedSubstrings([]string{
"resourceCount: Int(1)",
})

data := &telemetryData{
ResourceCount: 1,
}

err := exporter.Export(ctx, data)
Expect(err).ToNot(HaveOccurred())

Eventually(lc.unmatchedCount, "10s").Should(BeZero())
})
})
13 changes: 13 additions & 0 deletions tests/tests_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package tests

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestExporter(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Tests Suite")
}

0 comments on commit 84dcb16

Please sign in to comment.