refactor(telemetry): split telemetry between operator and controller
Split telemetry collection between cozystack-operator and cozystack-controller: cozystack-operator now collects cluster-level metrics: - cozy_cluster_info (cozystack_version, kubernetes_version) - cozy_nodes_count (os, kernel) - cozy_cluster_capacity (cpu, memory, nvidia.com/* resources) - cozy_loadbalancers_count - cozy_pvs_count (driver, size) - cozy_package_info (name, variant) cozystack-controller now collects application-level metrics: - cozy_application_count (kind) - counts HelmReleases per ApplicationDefinition Other changes: - Add pkg/version for build-time version injection via ldflags - Remove --cozystack-version flag (version now embedded at build time) - Remove bundle/oidc configuration from telemetry (replaced by package_info) - Remove cozy_workloads_count metric (replaced by cozy_application_count) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
This commit is contained in:
parent
bfd2e1fd15
commit
987a74ae5a
16 changed files with 415 additions and 225 deletions
|
|
@ -68,7 +68,6 @@ func main() {
|
|||
var disableTelemetry bool
|
||||
var telemetryEndpoint string
|
||||
var telemetryInterval string
|
||||
var cozystackVersion string
|
||||
var reconcileDeployment bool
|
||||
var tlsOpts []func(*tls.Config)
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
|
||||
|
|
@ -87,8 +86,6 @@ func main() {
|
|||
"Endpoint for sending telemetry data")
|
||||
flag.StringVar(&telemetryInterval, "telemetry-interval", "15m",
|
||||
"Interval between telemetry data collection (e.g. 15m, 1h)")
|
||||
flag.StringVar(&cozystackVersion, "cozystack-version", "unknown",
|
||||
"Version of Cozystack")
|
||||
flag.BoolVar(&reconcileDeployment, "reconcile-deployment", false,
|
||||
"If set, the Cozystack API server is assumed to run as a Deployment, else as a DaemonSet.")
|
||||
opts := zap.Options{
|
||||
|
|
@ -106,10 +103,9 @@ func main() {
|
|||
|
||||
// Configure telemetry
|
||||
telemetryConfig := telemetry.Config{
|
||||
Disabled: disableTelemetry,
|
||||
Endpoint: telemetryEndpoint,
|
||||
Interval: interval,
|
||||
CozystackVersion: cozystackVersion,
|
||||
Disabled: disableTelemetry,
|
||||
Endpoint: telemetryEndpoint,
|
||||
Interval: interval,
|
||||
}
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ import (
|
|||
"github.com/cozystack/cozystack/internal/cozyvaluesreplicator"
|
||||
"github.com/cozystack/cozystack/internal/fluxinstall"
|
||||
"github.com/cozystack/cozystack/internal/operator"
|
||||
"github.com/cozystack/cozystack/internal/telemetry"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
|
|
@ -77,7 +78,9 @@ func main() {
|
|||
var secureMetrics bool
|
||||
var enableHTTP2 bool
|
||||
var installFlux bool
|
||||
var cozystackVersion string
|
||||
var disableTelemetry bool
|
||||
var telemetryEndpoint string
|
||||
var telemetryInterval string
|
||||
var cozyValuesSecretName string
|
||||
var cozyValuesSecretNamespace string
|
||||
var cozyValuesNamespaceSelector string
|
||||
|
|
@ -95,8 +98,12 @@ func main() {
|
|||
flag.BoolVar(&enableHTTP2, "enable-http2", false,
|
||||
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
|
||||
flag.BoolVar(&installFlux, "install-flux", false, "Install Flux components before starting reconcile loop")
|
||||
flag.StringVar(&cozystackVersion, "cozystack-version", "unknown",
|
||||
"Version of Cozystack")
|
||||
flag.BoolVar(&disableTelemetry, "disable-telemetry", false,
|
||||
"Disable telemetry collection")
|
||||
flag.StringVar(&telemetryEndpoint, "telemetry-endpoint", "https://telemetry.cozystack.io",
|
||||
"Endpoint for sending telemetry data")
|
||||
flag.StringVar(&telemetryInterval, "telemetry-interval", "15m",
|
||||
"Interval between telemetry data collection (e.g. 15m, 1h)")
|
||||
flag.StringVar(&platformSourceURL, "platform-source-url", "", "Platform source URL (oci:// or https://). If specified, generates OCIRepository or GitRepository resource.")
|
||||
flag.StringVar(&platformSourceName, "platform-source-name", "cozystack-packages", "Name for the generated platform source resource (default: cozystack-packages)")
|
||||
flag.StringVar(&platformSourceRef, "platform-source-ref", "", "Reference specification as key=value pairs (e.g., 'branch=main' or 'digest=sha256:...,tag=v1.0'). For OCI: digest, semver, semverFilter, tag. For Git: branch, tag, semver, name, commit.")
|
||||
|
|
@ -240,6 +247,32 @@ func main() {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Parse telemetry interval
|
||||
interval, err := time.ParseDuration(telemetryInterval)
|
||||
if err != nil {
|
||||
setupLog.Error(err, "invalid telemetry interval")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Configure telemetry
|
||||
telemetryConfig := telemetry.Config{
|
||||
Disabled: disableTelemetry,
|
||||
Endpoint: telemetryEndpoint,
|
||||
Interval: interval,
|
||||
}
|
||||
|
||||
// Initialize telemetry collector
|
||||
collector, err := telemetry.NewOperatorCollector(mgr.GetClient(), &telemetryConfig, config)
|
||||
if err != nil {
|
||||
setupLog.V(1).Info("unable to create telemetry collector, telemetry will be disabled", "error", err)
|
||||
}
|
||||
|
||||
if collector != nil {
|
||||
if err := mgr.Add(collector); err != nil {
|
||||
setupLog.V(1).Info("unable to set up telemetry collector, continuing without telemetry", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
setupLog.Info("Starting controller manager")
|
||||
mgrCtx := ctrl.SetupSignalHandler()
|
||||
if err := mgr.Start(mgrCtx); err != nil {
|
||||
|
|
|
|||
1
go.mod
1
go.mod
|
|
@ -19,7 +19,6 @@ require (
|
|||
github.com/spf13/cobra v1.9.1
|
||||
github.com/vmware-tanzu/velero v1.17.1
|
||||
go.uber.org/zap v1.27.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/api v0.34.1
|
||||
k8s.io/apiextensions-apiserver v0.34.1
|
||||
k8s.io/apimachinery v0.34.2
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -297,8 +297,6 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
|||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -9,35 +9,36 @@ import (
|
|||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
)
|
||||
|
||||
// Collector handles telemetry data collection and sending
|
||||
const (
|
||||
// ApplicationKindLabel is the label used to identify application kind on HelmReleases
|
||||
ApplicationKindLabel = "apps.cozystack.io/application.kind"
|
||||
// UILabel is the label used to identify UI-managed HelmReleases
|
||||
UILabel = "cozystack.io/ui"
|
||||
)
|
||||
|
||||
// Collector handles telemetry data collection for cozystack-controller
|
||||
type Collector struct {
|
||||
client client.Client
|
||||
discoveryClient discovery.DiscoveryInterface
|
||||
config *Config
|
||||
ticker *time.Ticker
|
||||
stopCh chan struct{}
|
||||
client client.Client
|
||||
config *Config
|
||||
ticker *time.Ticker
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
// NewCollector creates a new telemetry collector
|
||||
func NewCollector(client client.Client, config *Config, kubeConfig *rest.Config) (*Collector, error) {
|
||||
discoveryClient, err := discovery.NewDiscoveryClientForConfig(kubeConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create discovery client: %w", err)
|
||||
}
|
||||
// NewCollector creates a new telemetry collector for cozystack-controller
|
||||
func NewCollector(c client.Client, config *Config, _ *rest.Config) (*Collector, error) {
|
||||
return &Collector{
|
||||
client: client,
|
||||
discoveryClient: discoveryClient,
|
||||
config: config,
|
||||
client: c,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -67,46 +68,9 @@ func (c *Collector) Start(ctx context.Context) error {
|
|||
|
||||
// NeedLeaderElection implements manager.LeaderElectionRunnable
|
||||
func (c *Collector) NeedLeaderElection() bool {
|
||||
// Only run telemetry collector on the leader
|
||||
return true
|
||||
}
|
||||
|
||||
// Stop halts telemetry collection
|
||||
func (c *Collector) Stop() {
|
||||
close(c.stopCh)
|
||||
}
|
||||
|
||||
// getSizeGroup returns the exponential size group for PVC
|
||||
func getSizeGroup(size resource.Quantity) string {
|
||||
gb := size.Value() / (1024 * 1024 * 1024)
|
||||
switch {
|
||||
case gb <= 1:
|
||||
return "1Gi"
|
||||
case gb <= 5:
|
||||
return "5Gi"
|
||||
case gb <= 10:
|
||||
return "10Gi"
|
||||
case gb <= 25:
|
||||
return "25Gi"
|
||||
case gb <= 50:
|
||||
return "50Gi"
|
||||
case gb <= 100:
|
||||
return "100Gi"
|
||||
case gb <= 250:
|
||||
return "250Gi"
|
||||
case gb <= 500:
|
||||
return "500Gi"
|
||||
case gb <= 1024:
|
||||
return "1Ti"
|
||||
case gb <= 2048:
|
||||
return "2Ti"
|
||||
case gb <= 5120:
|
||||
return "5Ti"
|
||||
default:
|
||||
return "10Ti"
|
||||
}
|
||||
}
|
||||
|
||||
// collect gathers and sends telemetry data
|
||||
func (c *Collector) collect(ctx context.Context) {
|
||||
logger := log.FromContext(ctx).V(1)
|
||||
|
|
@ -120,151 +84,54 @@ func (c *Collector) collect(ctx context.Context) {
|
|||
|
||||
clusterID := string(kubeSystemNS.UID)
|
||||
|
||||
var cozystackCM corev1.ConfigMap
|
||||
if err := c.client.Get(ctx, types.NamespacedName{Namespace: "cozy-system", Name: "cozystack"}, &cozystackCM); err != nil {
|
||||
logger.Info(fmt.Sprintf("Failed to get cozystack configmap in cozy-system namespace: %v", err))
|
||||
// Get all ApplicationDefinitions to know which kinds exist
|
||||
var appDefList cozyv1alpha1.ApplicationDefinitionList
|
||||
if err := c.client.List(ctx, &appDefList); err != nil {
|
||||
logger.Info(fmt.Sprintf("Failed to list ApplicationDefinitions: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
oidcEnabled := cozystackCM.Data["oidc-enabled"]
|
||||
bundle := cozystackCM.Data["bundle-name"]
|
||||
bundleEnable := cozystackCM.Data["bundle-enable"]
|
||||
bundleDisable := cozystackCM.Data["bundle-disable"]
|
||||
// Build a map of all known application kinds (initialized with 0)
|
||||
appKindCounts := make(map[string]int)
|
||||
for _, appDef := range appDefList.Items {
|
||||
kind := appDef.Spec.Application.Kind
|
||||
if kind != "" {
|
||||
appKindCounts[kind] = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Get Kubernetes version from nodes
|
||||
var nodeList corev1.NodeList
|
||||
if err := c.client.List(ctx, &nodeList); err != nil {
|
||||
logger.Info(fmt.Sprintf("Failed to list nodes: %v", err))
|
||||
// Get all HelmReleases with cozystack.io/ui=true label in one request
|
||||
var hrList helmv2.HelmReleaseList
|
||||
if err := c.client.List(ctx, &hrList, client.MatchingLabels{UILabel: "true"}); err != nil {
|
||||
logger.Info(fmt.Sprintf("Failed to list HelmReleases: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Count HelmReleases by application kind
|
||||
for _, hr := range hrList.Items {
|
||||
kind := hr.Labels[ApplicationKindLabel]
|
||||
if kind != "" {
|
||||
appKindCounts[kind]++
|
||||
}
|
||||
}
|
||||
|
||||
// Create metrics buffer
|
||||
var metrics strings.Builder
|
||||
|
||||
// Add Cozystack info metric
|
||||
if len(nodeList.Items) > 0 {
|
||||
k8sVersion, _ := c.discoveryClient.ServerVersion()
|
||||
// Write application count metrics
|
||||
for kind, count := range appKindCounts {
|
||||
metrics.WriteString(fmt.Sprintf(
|
||||
"cozy_cluster_info{cozystack_version=\"%s\",kubernetes_version=\"%s\",oidc_enabled=\"%s\",bundle_name=\"%s\",bunde_enable=\"%s\",bunde_disable=\"%s\"} 1\n",
|
||||
c.config.CozystackVersion,
|
||||
k8sVersion,
|
||||
oidcEnabled,
|
||||
bundle,
|
||||
bundleEnable,
|
||||
bundleDisable,
|
||||
))
|
||||
}
|
||||
|
||||
// Collect node metrics
|
||||
nodeOSCount := make(map[string]int)
|
||||
for _, node := range nodeList.Items {
|
||||
key := fmt.Sprintf("%s (%s)", node.Status.NodeInfo.OperatingSystem, node.Status.NodeInfo.OSImage)
|
||||
nodeOSCount[key] = nodeOSCount[key] + 1
|
||||
}
|
||||
|
||||
for osKey, count := range nodeOSCount {
|
||||
metrics.WriteString(fmt.Sprintf(
|
||||
"cozy_nodes_count{os=\"%s\",kernel=\"%s\"} %d\n",
|
||||
osKey,
|
||||
nodeList.Items[0].Status.NodeInfo.KernelVersion,
|
||||
"cozy_application_count{kind=\"%s\"} %d\n",
|
||||
kind,
|
||||
count,
|
||||
))
|
||||
}
|
||||
|
||||
// Collect LoadBalancer services metrics
|
||||
var serviceList corev1.ServiceList
|
||||
if err := c.client.List(ctx, &serviceList); err != nil {
|
||||
logger.Info(fmt.Sprintf("Failed to list Services: %v", err))
|
||||
} else {
|
||||
lbCount := 0
|
||||
for _, svc := range serviceList.Items {
|
||||
if svc.Spec.Type == corev1.ServiceTypeLoadBalancer {
|
||||
lbCount++
|
||||
}
|
||||
// Send metrics only if there's something to send
|
||||
if metrics.Len() > 0 {
|
||||
if err := c.sendMetrics(clusterID, metrics.String()); err != nil {
|
||||
logger.Info(fmt.Sprintf("Failed to send metrics: %v", err))
|
||||
}
|
||||
metrics.WriteString(fmt.Sprintf("cozy_loadbalancers_count %d\n", lbCount))
|
||||
}
|
||||
|
||||
// Count tenant namespaces
|
||||
var nsList corev1.NamespaceList
|
||||
if err := c.client.List(ctx, &nsList); err != nil {
|
||||
logger.Info(fmt.Sprintf("Failed to list Namespaces: %v", err))
|
||||
} else {
|
||||
tenantCount := 0
|
||||
for _, ns := range nsList.Items {
|
||||
if strings.HasPrefix(ns.Name, "tenant-") {
|
||||
tenantCount++
|
||||
}
|
||||
}
|
||||
metrics.WriteString(fmt.Sprintf("cozy_tenants_count %d\n", tenantCount))
|
||||
}
|
||||
|
||||
// Collect PV metrics grouped by driver and size
|
||||
var pvList corev1.PersistentVolumeList
|
||||
if err := c.client.List(ctx, &pvList); err != nil {
|
||||
logger.Info(fmt.Sprintf("Failed to list PVs: %v", err))
|
||||
} else {
|
||||
// Map to store counts by size and driver
|
||||
pvMetrics := make(map[string]map[string]int)
|
||||
|
||||
for _, pv := range pvList.Items {
|
||||
if capacity, ok := pv.Spec.Capacity[corev1.ResourceStorage]; ok {
|
||||
sizeGroup := getSizeGroup(capacity)
|
||||
|
||||
// Get the CSI driver name
|
||||
driver := "unknown"
|
||||
if pv.Spec.CSI != nil {
|
||||
driver = pv.Spec.CSI.Driver
|
||||
} else if pv.Spec.HostPath != nil {
|
||||
driver = "hostpath"
|
||||
} else if pv.Spec.NFS != nil {
|
||||
driver = "nfs"
|
||||
}
|
||||
|
||||
// Initialize nested map if needed
|
||||
if _, exists := pvMetrics[sizeGroup]; !exists {
|
||||
pvMetrics[sizeGroup] = make(map[string]int)
|
||||
}
|
||||
|
||||
// Increment count for this size/driver combination
|
||||
pvMetrics[sizeGroup][driver]++
|
||||
}
|
||||
}
|
||||
|
||||
// Write metrics
|
||||
for size, drivers := range pvMetrics {
|
||||
for driver, count := range drivers {
|
||||
metrics.WriteString(fmt.Sprintf(
|
||||
"cozy_pvs_count{driver=\"%s\",size=\"%s\"} %d\n",
|
||||
driver,
|
||||
size,
|
||||
count,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect workload metrics
|
||||
var monitorList cozyv1alpha1.WorkloadMonitorList
|
||||
if err := c.client.List(ctx, &monitorList); err != nil {
|
||||
logger.Info(fmt.Sprintf("Failed to list WorkloadMonitors: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, monitor := range monitorList.Items {
|
||||
metrics.WriteString(fmt.Sprintf(
|
||||
"cozy_workloads_count{uid=\"%s\",kind=\"%s\",type=\"%s\",version=\"%s\"} %d\n",
|
||||
monitor.UID,
|
||||
monitor.Spec.Kind,
|
||||
monitor.Spec.Type,
|
||||
monitor.Spec.Version,
|
||||
monitor.Status.ObservedReplicas,
|
||||
))
|
||||
}
|
||||
|
||||
// Send metrics
|
||||
if err := c.sendMetrics(clusterID, metrics.String()); err != nil {
|
||||
logger.Info(fmt.Sprintf("Failed to send metrics: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,16 +12,13 @@ type Config struct {
|
|||
Endpoint string
|
||||
// Interval between telemetry data collection
|
||||
Interval time.Duration
|
||||
// CozystackVersion represents the current version of Cozystack
|
||||
CozystackVersion string
|
||||
}
|
||||
|
||||
// DefaultConfig returns default telemetry configuration
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Disabled: false,
|
||||
Endpoint: "https://telemetry.cozystack.io",
|
||||
Interval: 15 * time.Minute,
|
||||
CozystackVersion: "unknown",
|
||||
Disabled: false,
|
||||
Endpoint: "https://telemetry.cozystack.io",
|
||||
Interval: 15 * time.Minute,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
282
internal/telemetry/operator_collector.go
Normal file
282
internal/telemetry/operator_collector.go
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
package telemetry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
"github.com/cozystack/cozystack/pkg/version"
|
||||
)
|
||||
|
||||
// OperatorCollector handles telemetry data collection for cozystack-operator
|
||||
type OperatorCollector struct {
|
||||
client client.Client
|
||||
discoveryClient discovery.DiscoveryInterface
|
||||
config *Config
|
||||
ticker *time.Ticker
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
// NewOperatorCollector creates a new telemetry collector for cozystack-operator
|
||||
func NewOperatorCollector(c client.Client, config *Config, kubeConfig *rest.Config) (*OperatorCollector, error) {
|
||||
discoveryClient, err := discovery.NewDiscoveryClientForConfig(kubeConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create discovery client: %w", err)
|
||||
}
|
||||
return &OperatorCollector{
|
||||
client: c,
|
||||
discoveryClient: discoveryClient,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start implements manager.Runnable
|
||||
func (c *OperatorCollector) Start(ctx context.Context) error {
|
||||
if c.config.Disabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.ticker = time.NewTicker(c.config.Interval)
|
||||
c.stopCh = make(chan struct{})
|
||||
|
||||
// Initial collection
|
||||
c.collect(ctx)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
c.ticker.Stop()
|
||||
close(c.stopCh)
|
||||
return nil
|
||||
case <-c.ticker.C:
|
||||
c.collect(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NeedLeaderElection implements manager.LeaderElectionRunnable
|
||||
func (c *OperatorCollector) NeedLeaderElection() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// getSizeGroup returns the exponential size group for PVC
|
||||
func getSizeGroup(size resource.Quantity) string {
|
||||
gb := size.Value() / (1024 * 1024 * 1024)
|
||||
switch {
|
||||
case gb <= 1:
|
||||
return "1Gi"
|
||||
case gb <= 5:
|
||||
return "5Gi"
|
||||
case gb <= 10:
|
||||
return "10Gi"
|
||||
case gb <= 25:
|
||||
return "25Gi"
|
||||
case gb <= 50:
|
||||
return "50Gi"
|
||||
case gb <= 100:
|
||||
return "100Gi"
|
||||
case gb <= 250:
|
||||
return "250Gi"
|
||||
case gb <= 500:
|
||||
return "500Gi"
|
||||
case gb <= 1024:
|
||||
return "1Ti"
|
||||
case gb <= 2048:
|
||||
return "2Ti"
|
||||
case gb <= 5120:
|
||||
return "5Ti"
|
||||
default:
|
||||
return "10Ti"
|
||||
}
|
||||
}
|
||||
|
||||
// collect gathers and sends telemetry data
|
||||
func (c *OperatorCollector) collect(ctx context.Context) {
|
||||
logger := log.FromContext(ctx).V(1)
|
||||
|
||||
// Get cluster ID from kube-system namespace
|
||||
var kubeSystemNS corev1.Namespace
|
||||
if err := c.client.Get(ctx, types.NamespacedName{Name: "kube-system"}, &kubeSystemNS); err != nil {
|
||||
logger.Info(fmt.Sprintf("Failed to get kube-system namespace: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
clusterID := string(kubeSystemNS.UID)
|
||||
|
||||
// Get Kubernetes version
|
||||
k8sVersion, err := c.discoveryClient.ServerVersion()
|
||||
if err != nil {
|
||||
logger.Info(fmt.Sprintf("Failed to get Kubernetes version: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Get nodes
|
||||
var nodeList corev1.NodeList
|
||||
if err := c.client.List(ctx, &nodeList); err != nil {
|
||||
logger.Info(fmt.Sprintf("Failed to list nodes: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Create metrics buffer
|
||||
var metrics strings.Builder
|
||||
|
||||
// Add cluster info metric
|
||||
metrics.WriteString(fmt.Sprintf(
|
||||
"cozy_cluster_info{cozystack_version=\"%s\",kubernetes_version=\"%s\"} 1\n",
|
||||
version.Version,
|
||||
k8sVersion.GitVersion,
|
||||
))
|
||||
|
||||
// Collect node metrics grouped by OS and kernel
|
||||
nodeOSCount := make(map[string]map[string]int) // os -> kernel -> count
|
||||
for _, node := range nodeList.Items {
|
||||
osKey := fmt.Sprintf("%s (%s)", node.Status.NodeInfo.OperatingSystem, node.Status.NodeInfo.OSImage)
|
||||
kernelKey := node.Status.NodeInfo.KernelVersion
|
||||
|
||||
if _, exists := nodeOSCount[osKey]; !exists {
|
||||
nodeOSCount[osKey] = make(map[string]int)
|
||||
}
|
||||
nodeOSCount[osKey][kernelKey]++
|
||||
}
|
||||
|
||||
for osKey, kernels := range nodeOSCount {
|
||||
for kernel, count := range kernels {
|
||||
metrics.WriteString(fmt.Sprintf(
|
||||
"cozy_nodes_count{os=\"%s\",kernel=\"%s\"} %d\n",
|
||||
osKey,
|
||||
kernel,
|
||||
count,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Collect cluster capacity metrics (cpu, memory, gpu)
|
||||
capacityTotals := make(map[string]int64)
|
||||
for _, node := range nodeList.Items {
|
||||
for resourceName, quantity := range node.Status.Capacity {
|
||||
name := string(resourceName)
|
||||
if name == "cpu" || name == "memory" || strings.HasPrefix(name, "nvidia.com/") {
|
||||
capacityTotals[name] += quantity.Value()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for resourceName, total := range capacityTotals {
|
||||
metrics.WriteString(fmt.Sprintf(
|
||||
"cozy_cluster_capacity{resource=\"%s\"} %d\n",
|
||||
resourceName,
|
||||
total,
|
||||
))
|
||||
}
|
||||
|
||||
// Collect LoadBalancer services metrics
|
||||
var serviceList corev1.ServiceList
|
||||
if err := c.client.List(ctx, &serviceList); err != nil {
|
||||
logger.Info(fmt.Sprintf("Failed to list Services: %v", err))
|
||||
} else {
|
||||
lbCount := 0
|
||||
for _, svc := range serviceList.Items {
|
||||
if svc.Spec.Type == corev1.ServiceTypeLoadBalancer {
|
||||
lbCount++
|
||||
}
|
||||
}
|
||||
metrics.WriteString(fmt.Sprintf("cozy_loadbalancers_count %d\n", lbCount))
|
||||
}
|
||||
|
||||
// Collect PV metrics grouped by driver and size
|
||||
var pvList corev1.PersistentVolumeList
|
||||
if err := c.client.List(ctx, &pvList); err != nil {
|
||||
logger.Info(fmt.Sprintf("Failed to list PVs: %v", err))
|
||||
} else {
|
||||
pvMetrics := make(map[string]map[string]int) // size -> driver -> count
|
||||
|
||||
for _, pv := range pvList.Items {
|
||||
if capacity, ok := pv.Spec.Capacity[corev1.ResourceStorage]; ok {
|
||||
sizeGroup := getSizeGroup(capacity)
|
||||
|
||||
driver := "unknown"
|
||||
if pv.Spec.CSI != nil {
|
||||
driver = pv.Spec.CSI.Driver
|
||||
} else if pv.Spec.HostPath != nil {
|
||||
driver = "hostpath"
|
||||
} else if pv.Spec.NFS != nil {
|
||||
driver = "nfs"
|
||||
}
|
||||
|
||||
if _, exists := pvMetrics[sizeGroup]; !exists {
|
||||
pvMetrics[sizeGroup] = make(map[string]int)
|
||||
}
|
||||
pvMetrics[sizeGroup][driver]++
|
||||
}
|
||||
}
|
||||
|
||||
for size, drivers := range pvMetrics {
|
||||
for driver, count := range drivers {
|
||||
metrics.WriteString(fmt.Sprintf(
|
||||
"cozy_pvs_count{driver=\"%s\",size=\"%s\"} %d\n",
|
||||
driver,
|
||||
size,
|
||||
count,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect installed packages
|
||||
var packageList cozyv1alpha1.PackageList
|
||||
if err := c.client.List(ctx, &packageList); err != nil {
|
||||
logger.Info(fmt.Sprintf("Failed to list Packages: %v", err))
|
||||
} else {
|
||||
for _, pkg := range packageList.Items {
|
||||
variant := pkg.Spec.Variant
|
||||
if variant == "" {
|
||||
variant = "default"
|
||||
}
|
||||
metrics.WriteString(fmt.Sprintf(
|
||||
"cozy_package_info{name=\"%s\",variant=\"%s\"} 1\n",
|
||||
pkg.Name,
|
||||
variant,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Send metrics
|
||||
if err := c.sendMetrics(clusterID, metrics.String()); err != nil {
|
||||
logger.Info(fmt.Sprintf("Failed to send metrics: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// sendMetrics sends collected metrics to the configured endpoint
|
||||
func (c *OperatorCollector) sendMetrics(clusterID, metrics string) error {
|
||||
req, err := http.NewRequest("POST", c.config.Endpoint, bytes.NewBufferString(metrics))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "text/plain")
|
||||
req.Header.Set("X-Cluster-ID", clusterID)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -10,29 +10,26 @@ show:
|
|||
cozyhr show --namespace $(NAMESPACE) $(NAME) --plain
|
||||
|
||||
apply:
|
||||
cozyhr show --namespace $(NAMESPACE) $(NAME) --plain | kubectl apply --filename -
|
||||
cozyhr show --namespace $(NAMESPACE) $(NAME) --plain | kubectl apply -f -
|
||||
|
||||
diff:
|
||||
cozyhr show --namespace $(NAMESPACE) $(NAME) --plain | kubectl diff --filename -
|
||||
cozyhr show --namespace $(NAMESPACE) $(NAME) --plain | kubectl diff -f -
|
||||
|
||||
image: pre-checks image-operator image-packages
|
||||
|
||||
update-version:
|
||||
TAG="$(call settag,$(TAG))" \
|
||||
yq --inplace '.cozystackOperator.cozystackVersion = strenv(TAG)' values.yaml
|
||||
|
||||
image-operator:
|
||||
docker buildx build --file images/cozystack-operator/Dockerfile ../../.. \
|
||||
docker buildx build -f images/cozystack-operator/Dockerfile ../../.. \
|
||||
--tag $(REGISTRY)/cozystack-operator:$(call settag,$(TAG)) \
|
||||
--build-arg VERSION=$(call settag,$(TAG)) \
|
||||
--cache-from type=registry,ref=$(REGISTRY)/cozystack-operator:latest \
|
||||
--cache-to type=inline \
|
||||
--metadata-file images/cozystack-operator.json \
|
||||
$(BUILDX_ARGS)
|
||||
IMAGE="$(REGISTRY)/cozystack-operator:$(call settag,$(TAG))@$$(yq -e '.["containerimage.digest"]' images/cozystack-operator.json -o json -r)" \
|
||||
yq --inplace '.cozystackOperator.image = strenv(IMAGE)' values.yaml
|
||||
yq -i '.cozystackOperator.image = strenv(IMAGE)' values.yaml
|
||||
rm -f images/cozystack-operator.json
|
||||
|
||||
image-packages: update-version
|
||||
image-packages:
|
||||
mkdir -p ../../../_out/assets images
|
||||
flux push artifact \
|
||||
oci://$(REGISTRY)/cozystack-packages:$(call settag,$(TAG)) \
|
||||
|
|
@ -41,8 +38,8 @@ image-packages: update-version
|
|||
--revision="$$(git describe --tags):$$(git rev-parse HEAD)" \
|
||||
2>&1 | tee images/cozystack-packages.log
|
||||
export REPO="oci://$(REGISTRY)/cozystack-packages" \
|
||||
export DIGEST=$$(awk --field-separator @ '/artifact successfully pushed/ {print $$2}' images/cozystack-packages.log) && \
|
||||
export DIGEST=$$(awk -F @ '/artifact successfully pushed/ {print $$2}' images/cozystack-packages.log) && \
|
||||
rm -f images/cozystack-packages.log && \
|
||||
test -n "$$DIGEST" && \
|
||||
yq --inplace '.cozystackOperator.platformSourceUrl = strenv(REPO)' values.yaml && \
|
||||
yq --inplace '.cozystackOperator.platformSourceRef = "digest=" + strenv(DIGEST)' values.yaml
|
||||
yq -i '.cozystackOperator.platformSourceUrl = strenv(REPO)' values.yaml && \
|
||||
yq -i '.cozystackOperator.platformSourceRef = "digest=" + strenv(DIGEST)' values.yaml
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ FROM golang:1.25-alpine as builder
|
|||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG VERSION=dev
|
||||
|
||||
RUN apk add --no-cache make git
|
||||
|
||||
|
|
@ -12,7 +13,7 @@ RUN go mod download
|
|||
|
||||
# Build cozystack-operator
|
||||
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
|
||||
-ldflags="-w -s" \
|
||||
-ldflags="-w -s -X github.com/cozystack/cozystack/pkg/version.Version=${VERSION}" \
|
||||
-o /cozystack-operator \
|
||||
./cmd/cozystack-operator
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ spec:
|
|||
- --install-flux=true
|
||||
- --metrics-bind-address=0
|
||||
- --health-probe-bind-address=
|
||||
- --cozystack-version={{ .Values.cozystackOperator.cozystackVersion }}
|
||||
{{- if .Values.cozystackOperator.disableTelemetry }}
|
||||
- --disable-telemetry
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -2,4 +2,3 @@ cozystackOperator:
|
|||
image: ghcr.io/cozystack/cozystack/cozystack-operator:v1.0.0-alpha.1@sha256:d1beb9728b5be3ff6ee59eab071ac739cbb69d95f4bae173baa56323e97b0e5a
|
||||
platformSourceUrl: 'oci://ghcr.io/cozystack/cozystack/cozystack-packages'
|
||||
platformSourceRef: 'digest=sha256:ab03446b256c0faa9de0656854319f3b56e1252dab6f7b51bc6c95609d192556'
|
||||
cozystackVersion: v1.0.0-alpha.1
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ NAMESPACE=cozy-system
|
|||
include ../../../hack/common-envs.mk
|
||||
include ../../../hack/package.mk
|
||||
|
||||
image: image-cozystack-controller update-version
|
||||
image: image-cozystack-controller
|
||||
|
||||
image-cozystack-controller:
|
||||
docker buildx build -f images/cozystack-controller/Dockerfile ../../.. \
|
||||
--tag $(REGISTRY)/cozystack-controller:$(call settag,$(TAG)) \
|
||||
--build-arg VERSION=$(call settag,$(TAG)) \
|
||||
--cache-from type=registry,ref=$(REGISTRY)/cozystack-controller:latest \
|
||||
--cache-to type=inline \
|
||||
--metadata-file images/cozystack-controller.json \
|
||||
|
|
@ -16,7 +17,3 @@ image-cozystack-controller:
|
|||
IMAGE="$(REGISTRY)/cozystack-controller:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/cozystack-controller.json -o json -r)" \
|
||||
yq -i '.cozystackController.image = strenv(IMAGE)' values.yaml
|
||||
rm -f images/cozystack-controller.json
|
||||
|
||||
update-version:
|
||||
TAG="$(call settag,$(TAG))" \
|
||||
yq -i '.cozystackController.cozystackVersion = strenv(TAG)' values.yaml
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ FROM golang:1.25-alpine AS builder
|
|||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG VERSION=dev
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
|
|
@ -13,7 +14,9 @@ COPY pkg pkg/
|
|||
COPY cmd cmd/
|
||||
COPY internal internal/
|
||||
|
||||
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags="-extldflags=-static" -o /cozystack-controller cmd/cozystack-controller/main.go
|
||||
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build \
|
||||
-ldflags="-extldflags=-static -X github.com/cozystack/cozystack/pkg/version.Version=${VERSION}" \
|
||||
-o /cozystack-controller cmd/cozystack-controller/main.go
|
||||
|
||||
FROM scratch
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ spec:
|
|||
- name: cozystack-controller
|
||||
image: "{{ .Values.cozystackController.image }}"
|
||||
args:
|
||||
- --cozystack-version={{ .Values.cozystackController.cozystackVersion }}
|
||||
{{- if .Values.cozystackController.debug }}
|
||||
- --zap-log-level=debug
|
||||
{{- else }}
|
||||
|
|
|
|||
|
|
@ -2,5 +2,4 @@ cozystackController:
|
|||
image: ghcr.io/cozystack/cozystack/cozystack-controller:v1.0.0-alpha.1@sha256:b9c3f3ca0a65a7ed43d690e805ec42e1a7b3091418b7082e60478a82a5c8ddc3
|
||||
debug: false
|
||||
disableTelemetry: false
|
||||
cozystackVersion: "v1.0.0-alpha.1"
|
||||
cozystackAPIKind: "DaemonSet"
|
||||
|
|
|
|||
24
pkg/version/version.go
Normal file
24
pkg/version/version.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package version provides the version information for cozystack components.
|
||||
// Version is set at build time via -ldflags:
|
||||
//
|
||||
// go build -ldflags "-X github.com/cozystack/cozystack/pkg/version.Version=v1.0.0"
|
||||
package version
|
||||
|
||||
// Version is set at build time via -ldflags.
|
||||
var Version = "dev"
|
||||
Loading…
Add table
Add a link
Reference in a new issue