diff --git a/server/configuration.go b/server/configuration.go index b4a31e5..389c1b3 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -34,6 +34,10 @@ type configuration struct { HonorTimestamps *bool // Option to enable the experimental in-memory metadata storage and append metadata to the WAL. EnableMetadataStorage *bool + // Scrape interval is the time between polling the /metrics endpoint + ScrapeIntervalSeconds *int + // Screap timeout tells scraper to give up on the poll for a single scrape attempt + ScrapeTimeoutSeconds *int } func (c *configuration) SetDefaults() { @@ -61,9 +65,24 @@ func (c *configuration) SetDefaults() { if c.EnableMetadataStorage == nil { c.EnableMetadataStorage = model.NewBool(true) } + if c.ScrapeIntervalSeconds == nil { + c.ScrapeIntervalSeconds = model.NewInt(60) + } + if c.ScrapeTimeoutSeconds == nil { + c.ScrapeTimeoutSeconds = model.NewInt(10) + } } func (c *configuration) IsValid() error { + if *c.ScrapeIntervalSeconds < 1 { + return errors.New("scrape interval should be greater than zero") + } + if *c.ScrapeTimeoutSeconds < 1 { + return errors.New("scrape timeout should be greater than zero") + } + if *c.BodySizeLimitBytes < 100 { + return errors.New("openmetrics body size is not realistic, should be greater than 100 bytes") + } return nil } diff --git a/server/const.go b/server/const.go new file mode 100644 index 0000000..e5c2831 --- /dev/null +++ b/server/const.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/prometheus/prometheus/model/labels" +) + +var ( + sampleMutator func(labels.Labels) labels.Labels //nolint:unused +) diff --git a/server/plugin.go b/server/plugin.go index 4728081..ee92c09 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -3,11 +3,17 @@ package main import ( "fmt" "net/http" + "net/url" "sync" "time" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/v8/platform/shared/filestore" + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/config" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/model/relabel" + "github.com/prometheus/prometheus/scrape" "github.com/prometheus/prometheus/tsdb" ) @@ -54,6 +60,10 @@ func (p *Plugin) OnActivate() error { p.configuration.SetDefaults() } + if err = p.configuration.IsValid(); err != nil { + return fmt.Errorf("could not validate config: %w", err) + } + // check if cluster is enabled if p.isHA() { // TODO(isacikgoz): get cluster info @@ -72,6 +82,30 @@ func (p *Plugin) OnActivate() error { return fmt.Errorf("could not open target tsdb: %w", err) } + scrapeInterval := *p.configuration.ScrapeIntervalSeconds + + // TODO(isacikgoz): Use multiple targets for HA env + ls := labels.FromMap(map[string]string{ + model.AddressLabel: "localhost:8067", + model.ScrapeIntervalLabel: fmt.Sprintf("%ds", scrapeInterval), + model.ScrapeTimeoutLabel: fmt.Sprintf("%ds", *p.configuration.ScrapeTimeoutSeconds), + }) + lb := labels.NewBuilder(ls) + + lset, origLabels, err := scrape.PopulateLabels(lb, &config.ScrapeConfig{ + JobName: "prometheus", + }, true) + if err != nil { + return err + } + + target := scrape.NewTarget(lset, origLabels, url.Values{}) + + // Mutator is being used to apply labels to the metric samples + sampleMutator = func(l labels.Labels) labels.Labels { + return mutateSampleLabels(l, target, *p.configuration.HonorTimestamps, []*relabel.Config{}) + } + return nil } diff --git a/server/sample.go b/server/sample.go new file mode 100644 index 0000000..4b66a63 --- /dev/null +++ b/server/sample.go @@ -0,0 +1,60 @@ +package main + +import ( + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/model/relabel" + "github.com/prometheus/prometheus/scrape" + "golang.org/x/exp/slices" +) + +func mutateSampleLabels(lset labels.Labels, target *scrape.Target, honor bool, rc []*relabel.Config) labels.Labels { + lb := labels.NewBuilder(lset) + + if honor { + target.LabelsRange(func(l labels.Label) { + if !lset.Has(l.Name) { + lb.Set(l.Name, l.Value) + } + }) + } else { + var conflictingExposedLabels []labels.Label + target.LabelsRange(func(l labels.Label) { + existingValue := lset.Get(l.Name) + if existingValue != "" { + conflictingExposedLabels = append(conflictingExposedLabels, labels.Label{Name: l.Name, Value: existingValue}) + } + // It is now safe to set the target label. + lb.Set(l.Name, l.Value) + }) + + if len(conflictingExposedLabels) > 0 { + resolveConflictingExposedLabels(lb, conflictingExposedLabels) + } + } + + res := lb.Labels() + + if len(rc) > 0 { + res, _ = relabel.Process(res, rc...) + } + + return res +} + +func resolveConflictingExposedLabels(lb *labels.Builder, conflictingExposedLabels []labels.Label) { + slices.SortStableFunc(conflictingExposedLabels, func(a, b labels.Label) bool { + return len(a.Name) < len(b.Name) + }) + + for _, l := range conflictingExposedLabels { + newName := l.Name + for { + newName = model.ExportedLabelPrefix + newName + if lb.Get(newName) == "" { + lb.Set(newName, l.Value) + break + } + } + } +}