Ctrl K
ComunidadGuías…

config/project.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package config

import (
	"errors"
	"fmt"
	"net"
	"net/url"
	"time"

	"github.com/coroot/coroot/db"
	"github.com/coroot/coroot/model"
	"github.com/coroot/coroot/timeseries"
	"github.com/coroot/coroot/utils"
)

type RemoteCoroot struct {
	Url              string        `yaml:"url"`
	TlsSkipVerify    bool          `yaml:"tlsSkipVerify"`
	ApiKey           string        `yaml:"apiKey"`
	MetricResolution time.Duration `yaml:"metricResolution"`
}

func (rc *RemoteCoroot) Validate() error {
	if _, err := url.Parse(rc.Url); err != nil {
		return fmt.Errorf("invalid url %s: %w", rc.Url, err)
	}
	if len(rc.ApiKey) == 0 {
		return errors.New("missing api key")
	}
	if rc.MetricResolution < time.Second {
		return errors.New("metric_resolution too short")
	}
	return nil
}

func (rc *RemoteCoroot) ClickHouseConfig() *db.IntegrationClickhouse {
	u, _ := url.Parse(rc.Url)
	host, port, _ := net.SplitHostPort(u.Host)
	if host == "" {
		host = u.Host
	}
	if port == "" {
		if u.Scheme == "https" {
			port = "443"
		} else {
			port = "80"
		}
	}
	return &db.IntegrationClickhouse{
		Global:        true,
		Protocol:      "coroot",
		Addr:          net.JoinHostPort(host, port),
		Auth:          utils.BasicAuth{User: "default", Password: rc.ApiKey},
		Database:      "default",
		TlsEnable:     u.Scheme == "https",
		TlsSkipVerify: rc.TlsSkipVerify,
	}
}

func (rc *RemoteCoroot) PrometheusConfig() *db.IntegrationPrometheus {
	return &db.IntegrationPrometheus{
		Global:          true,
		Url:             rc.Url,
		RefreshInterval: timeseries.DurationFromStandard(rc.MetricResolution),
		TlsSkipVerify:   rc.TlsSkipVerify,
		CustomHeaders:   []utils.Header{{Key: "X-API-Key", Value: rc.ApiKey}},
	}
}

type AlertingRule struct {
	Id                   string                     `yaml:"id"`
	Name                 *string                    `yaml:"name,omitempty"`
	Source               *model.AlertSource         `yaml:"source,omitempty"`
	Selector             *model.AppSelector         `yaml:"selector,omitempty"`
	Severity             *string                    `yaml:"severity,omitempty"`
	For                  *timeseries.Duration       `yaml:"for,omitempty"`
	KeepFiringFor        *timeseries.Duration       `yaml:"keepFiringFor,omitempty"`
	Templates            *model.AlertTemplates      `yaml:"templates,omitempty"`
	NotificationCategory *model.ApplicationCategory `yaml:"notificationCategory,omitempty"`
	Enabled              *bool                      `yaml:"enabled,omitempty"`
}

func (ar *AlertingRule) Validate() error {
	if ar.Id == "" {
		return errors.New("id is required")
	}
	if ar.Source != nil {
		switch ar.Source.Type {
		case model.AlertSourceTypeCheck:
			if ar.Source.Check == nil || ar.Source.Check.CheckId == "" {
				return errors.New("source.check.checkId is required for check source")
			}
			configs := model.GetCheckConfigs()
			if _, ok := configs[ar.Source.Check.CheckId]; !ok {
				return fmt.Errorf("unknown checkId: %s", ar.Source.Check.CheckId)
			}
		case model.AlertSourceTypePromQL:
			if ar.Source.PromQL == nil || ar.Source.PromQL.Expression == "" {
				return errors.New("source.promql.expression is required for promql source")
			}
		case model.AlertSourceTypeLogPatterns:
			if ar.Source.LogPattern == nil || len(ar.Source.LogPattern.Severities) == 0 {
				return errors.New("source.logPattern.severities is required for log_patterns source")
			}
		case model.AlertSourceTypeKubernetesEvents:
			if ar.Source.KubernetesEvents == nil {
				return errors.New("source.kubernetesEvents is required for kubernetes_events source")
			}
		default:
			return fmt.Errorf("invalid source type: %s", ar.Source.Type)
		}
	}
	if ar.Severity != nil {
		switch *ar.Severity {
		case "warning", "critical":
		default:
			return fmt.Errorf("invalid severity: %s (must be 'warning' or 'critical')", *ar.Severity)
		}
	}
	return nil
}

func applyConfigOverrides(base *model.AlertingRule, override AlertingRule) *model.AlertingRule {
	result := *base
	if override.Name != nil {
		result.Name = *override.Name
	}
	if override.Source != nil {
		result.Source = *override.Source
	}
	if override.Selector != nil {
		result.Selector = *override.Selector
	}
	if override.Severity != nil {
		switch *override.Severity {
		case "warning":
			result.Severity = model.WARNING
		case "critical":
			result.Severity = model.CRITICAL
		}
	}
	if override.For != nil {
		result.For = *override.For
	}
	if override.KeepFiringFor != nil {
		result.KeepFiringFor = *override.KeepFiringFor
	}
	if override.Templates != nil {
		if override.Templates.Summary != "" {
			result.Templates.Summary = override.Templates.Summary
		}
		if override.Templates.Description != "" {
			result.Templates.Description = override.Templates.Description
		}
	}
	if override.NotificationCategory != nil {
		result.NotificationCategory = *override.NotificationCategory
	}
	if override.Enabled != nil {
		result.Enabled = *override.Enabled
	}
	return &result
}

type Project struct {
	Name string `yaml:"name"`

	MemberProjects []string `yaml:"memberProjects"`

	RemoteCoroot *RemoteCoroot `yaml:"remoteCoroot"`

	ApiKeys      []db.ApiKey `yaml:"apiKeys"`
	ApiKeysSnake []db.ApiKey `yaml:"api_keys"` // TODO: remove

	NotificationIntegrations *db.NotificationIntegrations `yaml:"notificationIntegrations"`
	ApplicationCategories    []ApplicationCategory        `yaml:"applicationCategories"`
	CustomApplications       []CustomApplication          `yaml:"customApplications"`

	AlertingRules []AlertingRule `yaml:"alertingRules"`

	InspectionOverrides *InspectionOverrides `yaml:"inspectionOverrides"`
}

func (p *Project) Validate() error {
	if p.Name == "" {
		return errors.New("name is required")
	}

	if len(p.ApiKeys) == 0 {
		p.ApiKeys = p.ApiKeysSnake
	}
	if len(p.ApiKeys) == 0 && len(p.MemberProjects) == 0 && p.RemoteCoroot == nil {
		return errors.New("no api keys defined")
	}
	for i, k := range p.ApiKeys {
		if err := k.Validate(); err != nil {
			return fmt.Errorf("invalid api key #%d: %w", i, err)
		}
	}

	if p.NotificationIntegrations != nil {
		if err := p.NotificationIntegrations.Validate(); err != nil {
			return fmt.Errorf("invalid notification integrations: %w", err)
		}
	}

	for i, c := range p.ApplicationCategories {
		if err := c.Validate(); err != nil {
			return fmt.Errorf("invalid application category #%d: %w", i, err)
		}
	}

	for i, c := range p.CustomApplications {
		if err := c.Validate(); err != nil {
			return fmt.Errorf("invalid custom application #%d: %w", i, err)
		}
	}
	for i, ar := range p.AlertingRules {
		if err := ar.Validate(); err != nil {
			return fmt.Errorf("invalid alerting rule #%d: %w", i, err)
		}
	}
	if p.InspectionOverrides != nil {
		if err := p.InspectionOverrides.Validate(); err != nil {
			return err
		}
	}
	if p.RemoteCoroot != nil {
		if err := p.RemoteCoroot.Validate(); err != nil {
			return fmt.Errorf("invalie remoteCoroot config: %w", err)
		}
	}
	return nil
}

type ApplicationCategory struct {
	Name                           model.ApplicationCategory `yaml:"name"`
	db.ApplicationCategorySettings `yaml:",inline"`
}

func (c *ApplicationCategory) Validate() error {
	if c.Name == "" {
		return errors.New("name is required")
	}
	return nil
}

type CustomApplication struct {
	Name                    string `yaml:"name"`
	model.CustomApplication `yaml:",inline"`
}

func (c *CustomApplication) Validate() error {
	if c.Name == "" {
		return errors.New("name is required")
	}
	return nil
}

type SLOAvailabilityOverride struct {
	ApplicationId    model.ApplicationId `yaml:"applicationId"`
	ObjectivePercent float32             `yaml:"objectivePercent"`
}

func (o *SLOAvailabilityOverride) Validate() error {
	if o.ObjectivePercent < 0 || o.ObjectivePercent > 100 {
		return errors.New("invalid objective_percent")
	}
	return nil
}

type SLOLatencyOverride struct {
	ApplicationId      model.ApplicationId `yaml:"applicationId"`
	ObjectivePercent   float32             `yaml:"objectivePercent"`
	ObjectiveThreshold time.Duration       `yaml:"objectiveThreshold"`
}

func (o *SLOLatencyOverride) Validate() error {
	return o.validate(model.DefaultHistogramBuckets, "[5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2.5s, 5s, 10s]")
}

func (o *SLOLatencyOverride) ValidateLLM() error {
	return o.validate(model.LLMHistogramBuckets, "[100ms, 250ms, 500ms, 1s, 2.5s, 5s, 10s, 20s, 30s, 1m, 2m, 5m, 10m]")
}

func (o *SLOLatencyOverride) validate(buckets []float32, bucketsDescription string) error {
	if o.ObjectivePercent < 0 || o.ObjectivePercent > 100 {
		return errors.New("invalid objective_percent")
	}
	bucket := model.RoundUpToBucket(float32(o.ObjectiveThreshold.Seconds()), buckets)
	if bucket > buckets[len(buckets)-1] {
		return fmt.Errorf("invalid objective_threshold: must match one of the standard buckets %s", bucketsDescription)
	}
	return nil
}

type InspectionOverrides struct {
	SLOAvailability []SLOAvailabilityOverride `yaml:"sloAvailability"`
	SLOLatency      []SLOLatencyOverride      `yaml:"sloLatency"`
	SLOLLMLatency   []SLOLatencyOverride      `yaml:"sloLLMLatency"`
}

func (io *InspectionOverrides) Validate() error {
	for _, o := range io.SLOAvailability {
		if err := o.Validate(); err != nil {
			return fmt.Errorf("invalid application availability SLO override for app %s: %w", o.ApplicationId.String(), err)
		}
	}
	for _, o := range io.SLOLatency {
		if err := o.Validate(); err != nil {
			return fmt.Errorf("invalid application latency SLO override for app %s: %w", o.ApplicationId.String(), err)
		}
	}
	for _, o := range io.SLOLLMLatency {
		if err := o.ValidateLLM(); err != nil {
			return fmt.Errorf("invalid application LLM latency SLO override for app %s: %w", o.ApplicationId.String(), err)
		}
	}
	return nil
}