From 3e459f7e4af646a7fcac0657a000d913b79ebf10 Mon Sep 17 00:00:00 2001 From: Jakub Jarosz <99677300+jjngx@users.noreply.github.com> Date: Tue, 12 Mar 2024 16:22:07 +0000 Subject: [PATCH] Feat/telemetry platform (#5217) --- internal/configs/configurator.go | 5 +- internal/telemetry/cluster.go | 39 +- internal/telemetry/cluster_test.go | 518 ++++++++++++++++++++++++++- internal/telemetry/collector.go | 90 +++-- internal/telemetry/collector_test.go | 247 +++++++------ internal/telemetry/exporter.go | 8 +- 6 files changed, 760 insertions(+), 147 deletions(-) diff --git a/internal/configs/configurator.go b/internal/configs/configurator.go index 8f057e1346..6ccd1e2b23 100644 --- a/internal/configs/configurator.go +++ b/internal/configs/configurator.go @@ -1479,8 +1479,9 @@ func (cnf *Configurator) GetIngressCounts() map[string]int { // GetVirtualServerCounts returns the total count of // VirtualServer and VirtualServerRoute resources that are handled by the Ingress Controller -func (cnf *Configurator) GetVirtualServerCounts() (vsCount int, vsrCount int) { - vsCount = len(cnf.virtualServers) +func (cnf *Configurator) GetVirtualServerCounts() (int, int) { + vsCount := len(cnf.virtualServers) + vsrCount := 0 for _, vs := range cnf.virtualServers { vsrCount += len(vs.VirtualServerRoutes) } diff --git a/internal/telemetry/cluster.go b/internal/telemetry/cluster.go index e3f16dd4c9..05e88edae3 100644 --- a/internal/telemetry/cluster.go +++ b/internal/telemetry/cluster.go @@ -2,18 +2,20 @@ package telemetry import ( "context" + "errors" + "strings" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // NodeCount returns the total number of nodes in the cluster. // It returns an error if the underlying k8s API client errors. -func (c *Collector) NodeCount(ctx context.Context) (int64, error) { +func (c *Collector) NodeCount(ctx context.Context) (int, error) { nodes, err := c.Config.K8sClientReader.CoreV1().Nodes().List(ctx, metaV1.ListOptions{}) if err != nil { return 0, err } - return int64(len(nodes.Items)), nil + return len(nodes.Items), nil } // ClusterID returns the UID of the kube-system namespace representing cluster id. @@ -35,3 +37,36 @@ func (c *Collector) ClusterVersion() (string, error) { } return sv.String(), nil } + +// Platform returns a string representing platform name. +func (c *Collector) Platform(ctx context.Context) (string, error) { + nodes, err := c.Config.K8sClientReader.CoreV1().Nodes().List(ctx, metaV1.ListOptions{}) + if err != nil { + return "", err + } + if len(nodes.Items) == 0 { + return "", errors.New("no nodes in the cluster, cannot determine platform name") + } + return lookupPlatform(nodes.Items[0].Spec.ProviderID), nil +} + +// lookupPlatform takes a string representing a K8s PlatformID +// retrieved from a cluster node and returns a string +// representing the platform name. +func lookupPlatform(providerID string) string { + provider := strings.TrimSpace(providerID) + if provider == "" { + return "other" + } + + provider = strings.ToLower(providerID) + + p := strings.Split(provider, ":") + if len(p) == 0 { + return "other" + } + if p[0] == "" { + return "other" + } + return p[0] +} diff --git a/internal/telemetry/cluster_test.go b/internal/telemetry/cluster_test.go index 162e576641..c37d78022c 100644 --- a/internal/telemetry/cluster_test.go +++ b/internal/telemetry/cluster_test.go @@ -19,7 +19,7 @@ func TestNodeCountInAClusterWithThreeNodes(t *testing.T) { if err != nil { t.Fatal(err) } - var want int64 = 3 + want := 3 if want != got { t.Errorf("want %v, got %v", want, got) } @@ -33,7 +33,7 @@ func TestNodeCountInAClusterWithOneNode(t *testing.T) { if err != nil { t.Fatal(err) } - var want int64 = 1 + want := 1 if want != got { t.Errorf("want %v, got %v", want, got) } @@ -80,6 +80,276 @@ func TestK8sVersionRetrievesClusterVersion(t *testing.T) { } } +func TestAWSPlatformDeterminesOwnName(t *testing.T) { + t.Parallel() + + c := newTestCollectorForClusterWithNodes(t, nodeAWS) + got, err := c.Platform(context.Background()) + if err != nil { + t.Fatal(err) + } + + want := "aws" + if want != got { + t.Errorf("want %s, got %s", want, got) + } +} + +func TestAzurePlatformDeterminesOwnName(t *testing.T) { + t.Parallel() + + c := newTestCollectorForClusterWithNodes(t, nodeAzure) + got, err := c.Platform(context.Background()) + if err != nil { + t.Fatal(err) + } + + want := "azure" + if want != got { + t.Errorf("want %s, got %s", want, got) + } +} + +func TestGCPPlatformDeterminesOwnName(t *testing.T) { + t.Parallel() + + c := newTestCollectorForClusterWithNodes(t, nodeGCP) + got, err := c.Platform(context.Background()) + if err != nil { + t.Fatal(err) + } + + want := "gce" + if want != got { + t.Errorf("want %s, got %s", want, got) + } +} + +func TestKindPlatformDeterminesOwnName(t *testing.T) { + t.Parallel() + + c := newTestCollectorForClusterWithNodes(t, nodeKind) + got, err := c.Platform(context.Background()) + if err != nil { + t.Fatal(err) + } + + want := "kind" + if want != got { + t.Errorf("want %s, got %s", want, got) + } +} + +func TestVSpherePlatformDeterminesOwnName(t *testing.T) { + t.Parallel() + + c := newTestCollectorForClusterWithNodes(t, nodeVSphere) + got, err := c.Platform(context.Background()) + if err != nil { + t.Fatal(err) + } + + want := "vsphere" + if want != got { + t.Errorf("want %s, got %s", want, got) + } +} + +func TestK3SPlatformDeterminesOwnName(t *testing.T) { + t.Parallel() + + c := newTestCollectorForClusterWithNodes(t, nodeK3S) + got, err := c.Platform(context.Background()) + if err != nil { + t.Fatal(err) + } + + want := "k3s" + if want != got { + t.Errorf("want %s, got %s", want, got) + } +} + +func TestIBMCloudPlatformDeterminesOwnName(t *testing.T) { + t.Parallel() + + c := newTestCollectorForClusterWithNodes(t, nodeIBMCloud) + got, err := c.Platform(context.Background()) + if err != nil { + t.Fatal(err) + } + + want := "ibmcloud" + if want != got { + t.Errorf("want %s, got %s", want, got) + } +} + +func TestIBMPowerPlatformDeterminesOwnName(t *testing.T) { + t.Parallel() + + c := newTestCollectorForClusterWithNodes(t, nodeIBMPowerVS) + got, err := c.Platform(context.Background()) + if err != nil { + t.Fatal(err) + } + + want := "ibmpowervs" + if want != got { + t.Errorf("want %s, got %s", want, got) + } +} + +func TestCloudStackPlatformDeterminesOwnName(t *testing.T) { + t.Parallel() + + c := newTestCollectorForClusterWithNodes(t, nodeCloudStack) + got, err := c.Platform(context.Background()) + if err != nil { + t.Fatal(err) + } + + want := "cloudstack" + if want != got { + t.Errorf("want %s, got %s", want, got) + } +} + +func TestOpenStackPlatformDeterminesOwnName(t *testing.T) { + t.Parallel() + + c := newTestCollectorForClusterWithNodes(t, nodeOpenStack) + got, err := c.Platform(context.Background()) + if err != nil { + t.Fatal(err) + } + + want := "openstack" + if want != got { + t.Errorf("want %s, got %s", want, got) + } +} + +func TestDigitalOceanPlatformDeterminesOwnName(t *testing.T) { + t.Parallel() + + c := newTestCollectorForClusterWithNodes(t, nodeDigitalOcean) + got, err := c.Platform(context.Background()) + if err != nil { + t.Fatal(err) + } + + want := "digitalocean" + if want != got { + t.Errorf("want %s, got %s", want, got) + } +} + +func TestEquinixMetallPlatformDeterminesOwnName(t *testing.T) { + t.Parallel() + + c := newTestCollectorForClusterWithNodes(t, nodeEquinixMetal) + got, err := c.Platform(context.Background()) + if err != nil { + t.Fatal(err) + } + + want := "equinixmetal" + if want != got { + t.Errorf("want %s, got %s", want, got) + } +} + +func TestAlibabaPlatformDeterminesOwnName(t *testing.T) { + t.Parallel() + + c := newTestCollectorForClusterWithNodes(t, nodeAlibaba) + got, err := c.Platform(context.Background()) + if err != nil { + t.Fatal(err) + } + + want := "alicloud" + if want != got { + t.Errorf("want %s, got %s", want, got) + } +} + +func TestPlatformLookupOnMissingPlatformIDField(t *testing.T) { + t.Parallel() + + c := newTestCollectorForClusterWithNodes(t, node1) + got, err := c.Platform(context.Background()) + if err != nil { + t.Fatal(err) + } + + want := "other" + if want != got { + t.Errorf("want %s, got %s", want, got) + } +} + +func TestPlatformLookupOnMalformedPlatformIDField(t *testing.T) { + t.Parallel() + + c := newTestCollectorForClusterWithNodes(t, nodeMalformedPlatformID) + got, err := c.Platform(context.Background()) + if err != nil { + t.Fatal(err) + } + + want := "//4232e3c7-d83c-d72b-758c-71d07a3d9310" + if want != got { + t.Errorf("want %s, got %s", want, got) + } +} + +func TestPlatformLookupOnMalformedBlankPlatformIDField(t *testing.T) { + t.Parallel() + + c := newTestCollectorForClusterWithNodes(t, nodeMalformedBlankPlatformID) + got, err := c.Platform(context.Background()) + if err != nil { + t.Fatal(err) + } + + want := "other" + if want != got { + t.Errorf("want %s, got %s", want, got) + } +} + +func TestPlatformLookupOnMalformedEmptyPlatformIDField(t *testing.T) { + t.Parallel() + + c := newTestCollectorForClusterWithNodes(t, nodeMalformedEmptyPlatformID) + got, err := c.Platform(context.Background()) + if err != nil { + t.Fatal(err) + } + + want := "other" + if want != got { + t.Errorf("want %s, got %s", want, got) + } +} + +func TestPlatformLookupOnMalformedPartialPlatformIDField(t *testing.T) { + t.Parallel() + + c := newTestCollectorForClusterWithNodes(t, nodeMalformedPartialPlatformID) + got, err := c.Platform(context.Background()) + if err != nil { + t.Fatal(err) + } + + want := "other" + if want != got { + t.Errorf("want %s, got %s", want, got) + } +} + // newTestCollectorForClusterWithNodes returns a telemetry collector configured // to simulate collecting data on a cluser with provided nodes. func newTestCollectorForClusterWithNodes(t *testing.T, nodes ...runtime.Object) *telemetry.Collector { @@ -156,3 +426,247 @@ var ( Spec: apiCoreV1.NamespaceSpec{}, } ) + +// Cloud providers' nodes for testing ProviderID lookups. +var ( + nodeAWS = &apiCoreV1.Node{ + TypeMeta: metaV1.TypeMeta{ + Kind: "Node", + APIVersion: "v1", + }, + ObjectMeta: metaV1.ObjectMeta{ + Name: "node", + Namespace: "default", + }, + Spec: apiCoreV1.NodeSpec{ + ProviderID: "aws:///eu-central-1a/i-088b4f07708408cc0", + }, + } + + nodeAzure = &apiCoreV1.Node{ + TypeMeta: metaV1.TypeMeta{ + Kind: "Node", + APIVersion: "v1", + }, + ObjectMeta: metaV1.ObjectMeta{ + Name: "node", + Namespace: "default", + }, + Spec: apiCoreV1.NodeSpec{ + ProviderID: "azure:///subscriptions/ba96ef31-4a42-40f5-8740-03f7e3c439eb/resourceGroups/mc_hibrid-weu_be3rr5ovr8ulf_westeurope/providers/Microsoft.Compute/virtualMachines/aks-pool1-27255451-0", + }, + } + + nodeGCP = &apiCoreV1.Node{ + TypeMeta: metaV1.TypeMeta{ + Kind: "Node", + APIVersion: "v1", + }, + ObjectMeta: metaV1.ObjectMeta{ + Name: "node", + Namespace: "default", + }, + Spec: apiCoreV1.NodeSpec{ + ProviderID: "gce://gcp-banzaidevgcp-nprd-38306/europe-north1-a/gke-vzf3z1vvleco9-pool1-7e48d363-8qz1", + }, + } + + nodeKind = &apiCoreV1.Node{ + TypeMeta: metaV1.TypeMeta{ + Kind: "Node", + APIVersion: "v1", + }, + ObjectMeta: metaV1.ObjectMeta{ + Name: "node", + Namespace: "default", + }, + Spec: apiCoreV1.NodeSpec{ + ProviderID: "kind://docker/local/local-control-plane", + }, + } + + nodeVSphere = &apiCoreV1.Node{ + TypeMeta: metaV1.TypeMeta{ + Kind: "Node", + APIVersion: "v1", + }, + ObjectMeta: metaV1.ObjectMeta{ + Name: "node", + Namespace: "default", + }, + Spec: apiCoreV1.NodeSpec{ + ProviderID: "vsphere://4232e3c7-d83c-d72b-758c-71d07a3d9310", + }, + } + + nodeK3S = &apiCoreV1.Node{ + TypeMeta: metaV1.TypeMeta{ + Kind: "Node", + APIVersion: "v1", + }, + ObjectMeta: metaV1.ObjectMeta{ + Name: "node", + Namespace: "default", + }, + Spec: apiCoreV1.NodeSpec{ + ProviderID: "k3s://ip-1.2.3.4", + }, + } + + nodeIBMCloud = &apiCoreV1.Node{ + TypeMeta: metaV1.TypeMeta{ + Kind: "Node", + APIVersion: "v1", + }, + ObjectMeta: metaV1.ObjectMeta{ + Name: "node", + Namespace: "default", + }, + Spec: apiCoreV1.NodeSpec{ + ProviderID: "ibmcloud://4232e3c7-d83c-d72b-758c-71d07a3d9310", + }, + } + + nodeIBMPowerVS = &apiCoreV1.Node{ + TypeMeta: metaV1.TypeMeta{ + Kind: "Node", + APIVersion: "v1", + }, + ObjectMeta: metaV1.ObjectMeta{ + Name: "node", + Namespace: "default", + }, + Spec: apiCoreV1.NodeSpec{ + ProviderID: "ibmpowervs://4232e3c7-d83c-d72b-758c-71d07a3d9310", + }, + } + + nodeCloudStack = &apiCoreV1.Node{ + TypeMeta: metaV1.TypeMeta{ + Kind: "Node", + APIVersion: "v1", + }, + ObjectMeta: metaV1.ObjectMeta{ + Name: "node", + Namespace: "default", + }, + Spec: apiCoreV1.NodeSpec{ + ProviderID: "cloudstack://4232e3c7-d83c-d72b-758c-71d07a3d9310", + }, + } + + nodeOpenStack = &apiCoreV1.Node{ + TypeMeta: metaV1.TypeMeta{ + Kind: "Node", + APIVersion: "v1", + }, + ObjectMeta: metaV1.ObjectMeta{ + Name: "node", + Namespace: "default", + }, + Spec: apiCoreV1.NodeSpec{ + ProviderID: "openstack://4232e3c7-d83c-d72b-758c-71d07a3d9310", + }, + } + + nodeDigitalOcean = &apiCoreV1.Node{ + TypeMeta: metaV1.TypeMeta{ + Kind: "Node", + APIVersion: "v1", + }, + ObjectMeta: metaV1.ObjectMeta{ + Name: "node", + Namespace: "default", + }, + Spec: apiCoreV1.NodeSpec{ + ProviderID: "digitalocean://4232e3c7-d83c-d72b-758c-71d07a3d9310", + }, + } + + nodeEquinixMetal = &apiCoreV1.Node{ + TypeMeta: metaV1.TypeMeta{ + Kind: "Node", + APIVersion: "v1", + }, + ObjectMeta: metaV1.ObjectMeta{ + Name: "node", + Namespace: "default", + }, + Spec: apiCoreV1.NodeSpec{ + ProviderID: "equinixmetal://4232e3c7-d83c-d72b-758c-71d07a3d9310", + }, + } + + nodeAlibaba = &apiCoreV1.Node{ + TypeMeta: metaV1.TypeMeta{ + Kind: "Node", + APIVersion: "v1", + }, + ObjectMeta: metaV1.ObjectMeta{ + Name: "node", + Namespace: "default", + }, + Spec: apiCoreV1.NodeSpec{ + ProviderID: "alicloud://4232e3c7-d83c-d72b-758c-71d07a3d9310", + }, + } +) + +// Nodes with missing or malformed PorviderID. +var ( + nodeMalformedPlatformID = &apiCoreV1.Node{ + TypeMeta: metaV1.TypeMeta{ + Kind: "Node", + APIVersion: "v1", + }, + ObjectMeta: metaV1.ObjectMeta{ + Name: "node", + Namespace: "default", + }, + Spec: apiCoreV1.NodeSpec{ + ProviderID: "//4232e3c7-d83c-d72b-758c-71d07a3d9310", + }, + } + + nodeMalformedPartialPlatformID = &apiCoreV1.Node{ + TypeMeta: metaV1.TypeMeta{ + Kind: "Node", + APIVersion: "v1", + }, + ObjectMeta: metaV1.ObjectMeta{ + Name: "node", + Namespace: "default", + }, + Spec: apiCoreV1.NodeSpec{ + ProviderID: "://4232e3c7-d83c-d72b-758c-71d07a3d9310", + }, + } + + nodeMalformedEmptyPlatformID = &apiCoreV1.Node{ + TypeMeta: metaV1.TypeMeta{ + Kind: "Node", + APIVersion: "v1", + }, + ObjectMeta: metaV1.ObjectMeta{ + Name: "node", + Namespace: "default", + }, + Spec: apiCoreV1.NodeSpec{ + ProviderID: "", + }, + } + + nodeMalformedBlankPlatformID = &apiCoreV1.Node{ + TypeMeta: metaV1.TypeMeta{ + Kind: "Node", + APIVersion: "v1", + }, + ObjectMeta: metaV1.ObjectMeta{ + Name: "node", + Namespace: "default", + }, + Spec: apiCoreV1.NodeSpec{ + ProviderID: " ", + }, + } +) diff --git a/internal/telemetry/collector.go b/internal/telemetry/collector.go index a98fea9c72..583e9f424b 100644 --- a/internal/telemetry/collector.go +++ b/internal/telemetry/collector.go @@ -7,7 +7,7 @@ import ( "runtime" "time" - telemetry "github.com/nginxinc/telemetry-exporter/pkg/telemetry" + tel "github.com/nginxinc/telemetry-exporter/pkg/telemetry" "github.com/nginxinc/kubernetes-ingress/internal/configs" @@ -85,50 +85,92 @@ func (c *Collector) Start(ctx context.Context) { // It exports data using provided exporter. func (c *Collector) Collect(ctx context.Context) { glog.V(3).Info("Collecting telemetry data") - data, err := c.BuildReport(ctx) + report, err := c.BuildReport(ctx) if err != nil { glog.Errorf("Error collecting telemetry data: %v", err) } - err = c.Exporter.Export(ctx, data) - if err != nil { - glog.Errorf("Error exporting telemetry data: %v", err) - } - glog.V(3).Infof("Exported telemetry data: %+v", data) -} -// BuildReport takes context and builds report from gathered telemetry data. -func (c *Collector) BuildReport(ctx context.Context) (telemetry.Exportable, error) { - d := Data{ - Data: telemetry.Data{ - ProjectName: "NIC", + nicData := Data{ + tel.Data{ + ProjectName: report.Name, ProjectVersion: c.Config.Version, ProjectArchitecture: runtime.GOARCH, + ClusterID: report.ClusterID, + ClusterVersion: report.ClusterVersion, + ClusterPlatform: report.ClusterPlatform, + ClusterNodeCount: int64(report.ClusterNodeCount), + }, + NICResourceCounts{ + VirtualServers: int64(report.VirtualServers), + VirtualServerRoutes: int64(report.VirtualServerRoutes), + TransportServers: int64(report.TransportServers), }, } - var err error + err = c.Exporter.Export(ctx, &nicData) + if err != nil { + glog.Errorf("Error exporting telemetry data: %v", err) + } + glog.V(3).Infof("Exported telemetry data: %+v", nicData) +} + +// Report holds collected NIC telemetry data. It is the package internal +// data structure used for decoupling types between the NIC `telemetry` +// package and the imported `telemetry` exporter. +type Report struct { + Name string + Version string + Architecture string + ClusterID string + ClusterVersion string + ClusterPlatform string + ClusterNodeCount int + VirtualServers int + VirtualServerRoutes int + TransportServers int +} + +// BuildReport takes context, collects telemetry data and builds the report. +func (c *Collector) BuildReport(ctx context.Context) (Report, error) { + vsCount := 0 + vsrCount := 0 + tsCount := 0 if c.Config.Configurator != nil { - vsCount, vsrCount := c.Config.Configurator.GetVirtualServerCounts() - d.VirtualServers, d.VirtualServerRoutes = int64(vsCount), int64(vsrCount) - d.TransportServers = int64(c.Config.Configurator.GetTransportServerCounts()) + vsCount, vsrCount = c.Config.Configurator.GetVirtualServerCounts() + tsCount = c.Config.Configurator.GetTransportServerCounts() } - if d.ClusterID, err = c.ClusterID(ctx); err != nil { + clusterID, err := c.ClusterID(ctx) + if err != nil { glog.Errorf("Error collecting telemetry data: ClusterID: %v", err) } - if d.ClusterNodeCount, err = c.NodeCount(ctx); err != nil { + nodes, err := c.NodeCount(ctx) + if err != nil { glog.Errorf("Error collecting telemetry data: Nodes: %v", err) } - if d.ClusterVersion, err = c.ClusterVersion(); err != nil { + version, err := c.ClusterVersion() + if err != nil { glog.Errorf("Error collecting telemetry data: K8s Version: %v", err) } - // TODO: Get Cluster (k8s) platform. e.g. EKS, AWS, Openshift, etc... + platform, err := c.Platform(ctx) + if err != nil { + glog.Errorf("Error collecting telemetry data: Platform: %v", err) + } - // TODO: Get InstallationID - // example of how NGF gets this ID https://github.com/nginxinc/nginx-gateway-fabric/blob/f33db51fc9e05ccf98fc8cdae100772a5cc6775e/internal/mode/static/telemetry/collector.go#L244-L248 - return &d, err + return Report{ + Name: "NIC", + Version: c.Config.Version, + Architecture: runtime.GOARCH, + ClusterID: clusterID, + ClusterVersion: version, + ClusterPlatform: platform, + ClusterNodeCount: nodes, + VirtualServers: vsCount, + VirtualServerRoutes: vsrCount, + TransportServers: tsCount, + }, err } diff --git a/internal/telemetry/collector_test.go b/internal/telemetry/collector_test.go index c797dff1ae..dfba4ce8fb 100644 --- a/internal/telemetry/collector_test.go +++ b/internal/telemetry/collector_test.go @@ -16,7 +16,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/nginxinc/kubernetes-ingress/internal/telemetry" conf_v1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1" - exporter "github.com/nginxinc/telemetry-exporter/pkg/telemetry" + tel "github.com/nginxinc/telemetry-exporter/pkg/telemetry" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8sruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/version" @@ -24,13 +24,6 @@ import ( testClient "k8s.io/client-go/kubernetes/fake" ) -var commonData = exporter.Data{ - ProjectName: "NIC", - ProjectVersion: "3.5.0", - ClusterVersion: "v1.29.2", - ProjectArchitecture: runtime.GOARCH, -} - func TestCreateNewCollectorWithCustomReportingPeriod(t *testing.T) { t.Parallel() @@ -60,7 +53,7 @@ func TestCreateNewCollectorWithCustomExporter(t *testing.T) { cfg := telemetry.CollectorConfig{ K8sClientReader: newTestClientset(), Configurator: newConfigurator(t), - Version: "3.5.0", + Version: telemetryNICData.ProjectVersion, } c, err := telemetry.NewCollector(cfg, telemetry.WithExporter(exp)) if err != nil { @@ -69,10 +62,10 @@ func TestCreateNewCollectorWithCustomExporter(t *testing.T) { c.Collect(context.Background()) td := telemetry.Data{ - Data: exporter.Data{ - ProjectName: "NIC", - ProjectVersion: "3.5.0", - ClusterVersion: "v1.29.2", + Data: tel.Data{ + ProjectName: telemetryNICData.ProjectName, + ProjectVersion: telemetryNICData.ProjectVersion, + ClusterVersion: telemetryNICData.ClusterVersion, ProjectArchitecture: runtime.GOARCH, }, } @@ -91,7 +84,7 @@ func TestCollectNodeCountInClusterWithOneNode(t *testing.T) { cfg := telemetry.CollectorConfig{ Configurator: newConfigurator(t), K8sClientReader: newTestClientset(node1), - Version: "3.5.0", + Version: telemetryNICData.ProjectVersion, } c, err := telemetry.NewCollector(cfg, telemetry.WithExporter(exp)) @@ -101,12 +94,13 @@ func TestCollectNodeCountInClusterWithOneNode(t *testing.T) { c.Collect(context.Background()) td := telemetry.Data{ - Data: exporter.Data{ - ProjectName: "NIC", - ProjectVersion: "3.5.0", - ClusterVersion: "v1.29.2", + Data: tel.Data{ + ProjectName: telemetryNICData.ProjectName, + ProjectVersion: telemetryNICData.ProjectVersion, + ClusterVersion: telemetryNICData.ClusterVersion, ProjectArchitecture: runtime.GOARCH, ClusterNodeCount: 1, + ClusterPlatform: "other", }, NICResourceCounts: telemetry.NICResourceCounts{ VirtualServers: 0, @@ -130,7 +124,7 @@ func TestCollectNodeCountInClusterWithThreeNodes(t *testing.T) { cfg := telemetry.CollectorConfig{ Configurator: newConfigurator(t), K8sClientReader: newTestClientset(node1, node2, node3), - Version: "3.5.0", + Version: telemetryNICData.ProjectVersion, } c, err := telemetry.NewCollector(cfg, telemetry.WithExporter(exp)) @@ -139,20 +133,26 @@ func TestCollectNodeCountInClusterWithThreeNodes(t *testing.T) { } c.Collect(context.Background()) + telData := tel.Data{ + ProjectName: telemetryNICData.ProjectName, + ProjectVersion: telemetryNICData.ProjectVersion, + ClusterVersion: telemetryNICData.ClusterVersion, + ClusterPlatform: "other", + ProjectArchitecture: runtime.GOARCH, + ClusterNodeCount: 3, + } + + nicResourceCounts := telemetry.NICResourceCounts{ + VirtualServers: 0, + VirtualServerRoutes: 0, + TransportServers: 0, + } + td := telemetry.Data{ - Data: exporter.Data{ - ProjectName: "NIC", - ProjectVersion: "3.5.0", - ClusterVersion: "v1.29.2", - ProjectArchitecture: runtime.GOARCH, - ClusterNodeCount: 3, - }, - NICResourceCounts: telemetry.NICResourceCounts{ - VirtualServers: 0, - VirtualServerRoutes: 0, - TransportServers: 0, - }, + telData, + nicResourceCounts, } + want := fmt.Sprintf("%+v", &td) got := buf.String() if !cmp.Equal(want, got) { @@ -168,7 +168,7 @@ func TestCollectClusterIDInClusterWithOneNode(t *testing.T) { cfg := telemetry.CollectorConfig{ Configurator: newConfigurator(t), K8sClientReader: newTestClientset(node1, kubeNS), - Version: "3.5.0", + Version: telemetryNICData.ProjectVersion, } c, err := telemetry.NewCollector(cfg, telemetry.WithExporter(exp)) @@ -178,13 +178,14 @@ func TestCollectClusterIDInClusterWithOneNode(t *testing.T) { c.Collect(context.Background()) td := telemetry.Data{ - Data: exporter.Data{ - ProjectName: "NIC", - ProjectVersion: "3.5.0", - ClusterVersion: "v1.29.2", + Data: tel.Data{ + ProjectName: telemetryNICData.ProjectName, + ProjectVersion: telemetryNICData.ProjectVersion, + ClusterVersion: telemetryNICData.ClusterVersion, + ClusterPlatform: "other", ProjectArchitecture: runtime.GOARCH, ClusterNodeCount: 1, - ClusterID: "329766ff-5d78-4c9e-8736-7faad1f2e937", + ClusterID: telemetryNICData.ClusterID, }, NICResourceCounts: telemetry.NICResourceCounts{ VirtualServers: 0, @@ -199,29 +200,68 @@ func TestCollectClusterIDInClusterWithOneNode(t *testing.T) { } } +func TestCollectClusterVersion(t *testing.T) { + t.Parallel() + + buf := &bytes.Buffer{} + exp := &telemetry.StdoutExporter{Endpoint: buf} + cfg := telemetry.CollectorConfig{ + Configurator: newConfigurator(t), + K8sClientReader: newTestClientset(node1, kubeNS), + Version: telemetryNICData.ProjectVersion, + } + + c, err := telemetry.NewCollector(cfg, telemetry.WithExporter(exp)) + if err != nil { + t.Fatal(err) + } + c.Collect(context.Background()) + + telData := tel.Data{ + ProjectName: telemetryNICData.ProjectName, + ProjectVersion: telemetryNICData.ProjectVersion, + ProjectArchitecture: telemetryNICData.ProjectArchitecture, + ClusterNodeCount: 1, + ClusterID: telemetryNICData.ClusterID, + ClusterVersion: telemetryNICData.ClusterVersion, + ClusterPlatform: "other", + } + + nicResourceCounts := telemetry.NICResourceCounts{ + VirtualServers: 0, + VirtualServerRoutes: 0, + TransportServers: 0, + } + + td := telemetry.Data{ + telData, + nicResourceCounts, + } + + want := fmt.Sprintf("%+v", &td) + got := buf.String() + if !cmp.Equal(want, got) { + t.Error(cmp.Diff(want, got)) + } +} + func TestCountVirtualServers(t *testing.T) { t.Parallel() testCases := []struct { testName string - expectedTraceDataOnAdd *telemetry.Data - expectedTraceDataOnDelete *telemetry.Data + expectedTraceDataOnAdd telemetry.Report + expectedTraceDataOnDelete telemetry.Report virtualServers []*configs.VirtualServerEx deleteCount int }{ { testName: "Create and delete 1 VirtualServer", - expectedTraceDataOnAdd: &telemetry.Data{ - Data: commonData, - NICResourceCounts: telemetry.NICResourceCounts{ - VirtualServers: 1, - }, + expectedTraceDataOnAdd: telemetry.Report{ + VirtualServers: 1, }, - expectedTraceDataOnDelete: &telemetry.Data{ - Data: commonData, - NICResourceCounts: telemetry.NICResourceCounts{ - VirtualServers: 0, - }, + expectedTraceDataOnDelete: telemetry.Report{ + VirtualServers: 0, }, virtualServers: []*configs.VirtualServerEx{ { @@ -238,17 +278,11 @@ func TestCountVirtualServers(t *testing.T) { }, { testName: "Create 2 VirtualServers and delete 2", - expectedTraceDataOnAdd: &telemetry.Data{ - Data: commonData, - NICResourceCounts: telemetry.NICResourceCounts{ - VirtualServers: 2, - }, + expectedTraceDataOnAdd: telemetry.Report{ + VirtualServers: 2, }, - expectedTraceDataOnDelete: &telemetry.Data{ - Data: commonData, - NICResourceCounts: telemetry.NICResourceCounts{ - VirtualServers: 0, - }, + expectedTraceDataOnDelete: telemetry.Report{ + VirtualServers: 0, }, virtualServers: []*configs.VirtualServerEx{ { @@ -274,17 +308,11 @@ func TestCountVirtualServers(t *testing.T) { }, { testName: "Create 2 VirtualServers and delete 1", - expectedTraceDataOnAdd: &telemetry.Data{ - Data: commonData, - NICResourceCounts: telemetry.NICResourceCounts{ - VirtualServers: 2, - }, + expectedTraceDataOnAdd: telemetry.Report{ + VirtualServers: 2, }, - expectedTraceDataOnDelete: &telemetry.Data{ - Data: commonData, - NICResourceCounts: telemetry.NICResourceCounts{ - VirtualServers: 1, - }, + expectedTraceDataOnDelete: telemetry.Report{ + VirtualServers: 1, }, virtualServers: []*configs.VirtualServerEx{ { @@ -314,9 +342,9 @@ func TestCountVirtualServers(t *testing.T) { configurator := newConfigurator(t) c, err := telemetry.NewCollector(telemetry.CollectorConfig{ - K8sClientReader: newTestClientset(dummyKubeNS), + K8sClientReader: newTestClientset(kubeNS, node1), Configurator: configurator, - Version: "3.5.0", + Version: telemetryNICData.ProjectVersion, }) if err != nil { t.Fatal(err) @@ -334,8 +362,8 @@ func TestCountVirtualServers(t *testing.T) { t.Fatal(err) } - if !cmp.Equal(test.expectedTraceDataOnAdd, gotTraceDataOnAdd) { - t.Error(cmp.Diff(test.expectedTraceDataOnAdd, gotTraceDataOnAdd)) + if !cmp.Equal(test.expectedTraceDataOnAdd.VirtualServers, gotTraceDataOnAdd.VirtualServers) { + t.Error(cmp.Diff(test.expectedTraceDataOnAdd.VirtualServers, gotTraceDataOnAdd.VirtualServers)) } for i := 0; i < test.deleteCount; i++ { @@ -352,8 +380,8 @@ func TestCountVirtualServers(t *testing.T) { t.Fatal(err) } - if !cmp.Equal(test.expectedTraceDataOnDelete, gotTraceDataOnDelete) { - t.Error(cmp.Diff(test.expectedTraceDataOnDelete, gotTraceDataOnDelete)) + if !cmp.Equal(test.expectedTraceDataOnDelete.VirtualServers, gotTraceDataOnDelete.VirtualServers) { + t.Error(cmp.Diff(test.expectedTraceDataOnDelete.VirtualServers, gotTraceDataOnDelete.VirtualServers)) } } } @@ -363,24 +391,18 @@ func TestCountTransportServers(t *testing.T) { testCases := []struct { testName string - expectedTraceDataOnAdd *telemetry.Data - expectedTraceDataOnDelete *telemetry.Data + expectedTraceDataOnAdd telemetry.Report + expectedTraceDataOnDelete telemetry.Report transportServers []*configs.TransportServerEx deleteCount int }{ { testName: "Create and delete 1 TransportServer", - expectedTraceDataOnAdd: &telemetry.Data{ - Data: commonData, - NICResourceCounts: telemetry.NICResourceCounts{ - TransportServers: 1, - }, + expectedTraceDataOnAdd: telemetry.Report{ + TransportServers: 1, }, - expectedTraceDataOnDelete: &telemetry.Data{ - Data: commonData, - NICResourceCounts: telemetry.NICResourceCounts{ - TransportServers: 0, - }, + expectedTraceDataOnDelete: telemetry.Report{ + TransportServers: 0, }, transportServers: []*configs.TransportServerEx{ { @@ -401,17 +423,11 @@ func TestCountTransportServers(t *testing.T) { }, { testName: "Create 2 and delete 2 TransportServer", - expectedTraceDataOnAdd: &telemetry.Data{ - Data: commonData, - NICResourceCounts: telemetry.NICResourceCounts{ - TransportServers: 2, - }, + expectedTraceDataOnAdd: telemetry.Report{ + TransportServers: 2, }, - expectedTraceDataOnDelete: &telemetry.Data{ - Data: commonData, - NICResourceCounts: telemetry.NICResourceCounts{ - TransportServers: 0, - }, + expectedTraceDataOnDelete: telemetry.Report{ + TransportServers: 0, }, transportServers: []*configs.TransportServerEx{ { @@ -445,17 +461,11 @@ func TestCountTransportServers(t *testing.T) { }, { testName: "Create 2 and delete 1 TransportServer", - expectedTraceDataOnAdd: &telemetry.Data{ - Data: commonData, - NICResourceCounts: telemetry.NICResourceCounts{ - TransportServers: 2, - }, + expectedTraceDataOnAdd: telemetry.Report{ + TransportServers: 2, }, - expectedTraceDataOnDelete: &telemetry.Data{ - Data: commonData, - NICResourceCounts: telemetry.NICResourceCounts{ - TransportServers: 1, - }, + expectedTraceDataOnDelete: telemetry.Report{ + TransportServers: 1, }, transportServers: []*configs.TransportServerEx{ { @@ -493,9 +503,9 @@ func TestCountTransportServers(t *testing.T) { configurator := newConfigurator(t) c, err := telemetry.NewCollector(telemetry.CollectorConfig{ - K8sClientReader: newTestClientset(dummyKubeNS), + K8sClientReader: newTestClientset(kubeNS, node1), Configurator: configurator, - Version: "3.5.0", + Version: telemetryNICData.ProjectVersion, }) if err != nil { t.Fatal(err) @@ -513,8 +523,8 @@ func TestCountTransportServers(t *testing.T) { t.Fatal(err) } - if !cmp.Equal(test.expectedTraceDataOnAdd, gotTraceDataOnAdd) { - t.Error(cmp.Diff(test.expectedTraceDataOnAdd, gotTraceDataOnAdd)) + if !cmp.Equal(test.expectedTraceDataOnAdd.TransportServers, gotTraceDataOnAdd.TransportServers) { + t.Error(cmp.Diff(test.expectedTraceDataOnAdd.TransportServers, gotTraceDataOnAdd.TransportServers)) } for i := 0; i < test.deleteCount; i++ { @@ -531,8 +541,8 @@ func TestCountTransportServers(t *testing.T) { t.Fatal(err) } - if !cmp.Equal(test.expectedTraceDataOnDelete, gotTraceDataOnDelete) { - t.Error(cmp.Diff(test.expectedTraceDataOnDelete, gotTraceDataOnDelete)) + if !cmp.Equal(test.expectedTraceDataOnDelete.TransportServers, gotTraceDataOnDelete.TransportServers) { + t.Error(cmp.Diff(test.expectedTraceDataOnDelete.TransportServers, gotTraceDataOnDelete.TransportServers)) } } } @@ -610,3 +620,14 @@ const ( virtualServerTemplatePath = "../configs/version2/nginx-plus.virtualserver.tmpl" transportServerTemplatePath = "../configs/version2/nginx-plus.transportserver.tmpl" ) + +// telemetryNICData holds static test data for telemetry tests. +var telemetryNICData = tel.Data{ + ProjectName: "NIC", + ProjectVersion: "3.5.0", + ClusterVersion: "v1.29.2", + ProjectArchitecture: runtime.GOARCH, + ClusterID: "329766ff-5d78-4c9e-8736-7faad1f2e937", + ClusterNodeCount: 1, + ClusterPlatform: "other", +} diff --git a/internal/telemetry/exporter.go b/internal/telemetry/exporter.go index ff84584164..3969fa3852 100644 --- a/internal/telemetry/exporter.go +++ b/internal/telemetry/exporter.go @@ -5,12 +5,12 @@ import ( "fmt" "io" - "github.com/nginxinc/telemetry-exporter/pkg/telemetry" + tel "github.com/nginxinc/telemetry-exporter/pkg/telemetry" ) // Exporter interface for exporters. type Exporter interface { - Export(ctx context.Context, data telemetry.Exportable) error + Export(ctx context.Context, data tel.Exportable) error } // StdoutExporter represents a temporary telemetry data exporter. @@ -19,7 +19,7 @@ type StdoutExporter struct { } // Export takes context and trace data and writes to the endpoint. -func (e *StdoutExporter) Export(_ context.Context, data telemetry.Exportable) error { +func (e *StdoutExporter) Export(_ context.Context, data tel.Exportable) error { fmt.Fprintf(e.Endpoint, "%+v", data) return nil } @@ -28,7 +28,7 @@ func (e *StdoutExporter) Export(_ context.Context, data telemetry.Exportable) er // //go:generate go run -tags=generator github.com/nginxinc/telemetry-exporter/cmd/generator -type Data -scheme -scheme-protocol=NICProductTelemetry -scheme-df-datatype=nic-product-telemetry -scheme-namespace=ingress.nginx.com type Data struct { - telemetry.Data + tel.Data NICResourceCounts }