// Copyright 2018 The Prometheus 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 adapter // NOTE: you do not need to edit this file when implementing a custom sd. import ( "context" "encoding/json" "fmt" "log/slog" "os" "path/filepath" "reflect" "sort" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/model" "github.com/prometheus/prometheus/discovery" "github.com/prometheus/prometheus/discovery/targetgroup" ) type customSD struct { Targets []string `json:"targets"` Labels map[string]string `json:"labels"` } func fingerprint(group *targetgroup.Group) model.Fingerprint { groupFingerprint := model.LabelSet{}.Fingerprint() for _, targets := range group.Targets { groupFingerprint ^= targets.Fingerprint() } groupFingerprint ^= group.Labels.Fingerprint() return groupFingerprint } // Adapter runs an unknown service discovery implementation and converts its target groups // to JSON and writes to a file for file_sd. type Adapter struct { ctx context.Context disc discovery.Discoverer groups map[string]*customSD manager *discovery.Manager output string name string logger *slog.Logger } func mapToArray(m map[string]*customSD) []customSD { arr := make([]customSD, 0, len(m)) for _, v := range m { arr = append(arr, *v) } return arr } func generateTargetGroups(allTargetGroups map[string][]*targetgroup.Group) map[string]*customSD { groups := make(map[string]*customSD) for k, sdTargetGroups := range allTargetGroups { for _, group := range sdTargetGroups { newTargets := make([]string, 0) newLabels := make(map[string]string) for _, targets := range group.Targets { for _, target := range targets { newTargets = append(newTargets, string(target)) } } sort.Strings(newTargets) for name, value := range group.Labels { newLabels[string(name)] = string(value) } sdGroup := customSD{ Targets: newTargets, Labels: newLabels, } // Make a unique key, including group's fingerprint, in case the sd_type (map key) and group.Source is not unique. groupFingerprint := fingerprint(group) key := fmt.Sprintf("%s:%s:%s", k, group.Source, groupFingerprint.String()) groups[key] = &sdGroup } } return groups } // Parses incoming target groups updates. If the update contains changes to the target groups // Adapter already knows about, or new target groups, we Marshal to JSON and write to file. func (a *Adapter) refreshTargetGroups(allTargetGroups map[string][]*targetgroup.Group) { tempGroups := generateTargetGroups(allTargetGroups) if !reflect.DeepEqual(a.groups, tempGroups) { a.groups = tempGroups err := a.writeOutput() if err != nil { a.logger.With("component", "sd-adapter").Error("failed to write output", "err", err) } } } // Writes JSON formatted targets to output file. func (a *Adapter) writeOutput() error { arr := mapToArray(a.groups) b, _ := json.MarshalIndent(arr, "", " ") dir, _ := filepath.Split(a.output) tmpfile, err := os.CreateTemp(dir, "sd-adapter") if err != nil { return err } defer tmpfile.Close() _, err = tmpfile.Write(b) if err != nil { return err } // Close the file immediately for platforms (eg. Windows) that cannot move // a file while a process is holding a file handle. tmpfile.Close() err = os.Rename(tmpfile.Name(), a.output) if err != nil { return err } return nil } func (a *Adapter) runCustomSD(ctx context.Context) { updates := a.manager.SyncCh() for { select { case <-ctx.Done(): case allTargetGroups, ok := <-updates: // Handle the case that a target provider exits and closes the channel // before the context is done. if !ok { return } a.refreshTargetGroups(allTargetGroups) } } } // Run starts a Discovery Manager and the custom service discovery implementation. func (a *Adapter) Run() { //nolint:errcheck go a.manager.Run() a.manager.StartCustomProvider(a.ctx, a.name, a.disc) go a.runCustomSD(a.ctx) } // NewAdapter creates a new instance of Adapter. func NewAdapter(ctx context.Context, file, name string, d discovery.Discoverer, logger *slog.Logger, sdMetrics map[string]discovery.DiscovererMetrics, registerer prometheus.Registerer) *Adapter { return &Adapter{ ctx: ctx, disc: d, groups: make(map[string]*customSD), manager: discovery.NewManager(ctx, logger, registerer, sdMetrics), output: file, name: name, logger: logger, } }