1// Copyright 2017 The Prometheus Authors
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14package triton
15
16import (
17	"encoding/json"
18	"fmt"
19	"io/ioutil"
20	"net/http"
21	"time"
22
23	"github.com/prometheus/client_golang/prometheus"
24	"github.com/prometheus/common/log"
25	"github.com/prometheus/common/model"
26	"github.com/prometheus/prometheus/config"
27	"github.com/prometheus/prometheus/util/httputil"
28	"golang.org/x/net/context"
29)
30
31const (
32	tritonLabel             = model.MetaLabelPrefix + "triton_"
33	tritonLabelMachineId    = tritonLabel + "machine_id"
34	tritonLabelMachineAlias = tritonLabel + "machine_alias"
35	tritonLabelMachineBrand = tritonLabel + "machine_brand"
36	tritonLabelMachineImage = tritonLabel + "machine_image"
37	tritonLabelServerId     = tritonLabel + "server_id"
38	namespace               = "prometheus"
39)
40
41var (
42	refreshFailuresCount = prometheus.NewCounter(
43		prometheus.CounterOpts{
44			Name: "prometheus_sd_triton_refresh_failures_total",
45			Help: "The number of Triton-SD scrape failures.",
46		})
47	refreshDuration = prometheus.NewSummary(
48		prometheus.SummaryOpts{
49			Name: "prometheus_sd_triton_refresh_duration_seconds",
50			Help: "The duration of a Triton-SD refresh in seconds.",
51		})
52)
53
54func init() {
55	prometheus.MustRegister(refreshFailuresCount)
56	prometheus.MustRegister(refreshDuration)
57}
58
59type DiscoveryResponse struct {
60	Containers []struct {
61		ServerUUID  string `json:"server_uuid"`
62		VMAlias     string `json:"vm_alias"`
63		VMBrand     string `json:"vm_brand"`
64		VMImageUUID string `json:"vm_image_uuid"`
65		VMUUID      string `json:"vm_uuid"`
66	} `json:"containers"`
67}
68
69// Discovery periodically performs Triton-SD requests. It implements
70// the TargetProvider interface.
71type Discovery struct {
72	client   *http.Client
73	interval time.Duration
74	logger   log.Logger
75	sdConfig *config.TritonSDConfig
76}
77
78// New returns a new Discovery which periodically refreshes its targets.
79func New(logger log.Logger, conf *config.TritonSDConfig) (*Discovery, error) {
80	tls, err := httputil.NewTLSConfig(conf.TLSConfig)
81	if err != nil {
82		return nil, err
83	}
84
85	transport := &http.Transport{TLSClientConfig: tls}
86	client := &http.Client{Transport: transport}
87
88	return &Discovery{
89		client:   client,
90		interval: time.Duration(conf.RefreshInterval),
91		logger:   logger,
92		sdConfig: conf,
93	}, nil
94}
95
96// Run implements the TargetProvider interface.
97func (d *Discovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) {
98	defer close(ch)
99
100	ticker := time.NewTicker(d.interval)
101	defer ticker.Stop()
102
103	// Get an initial set right away.
104	tg, err := d.refresh()
105	if err != nil {
106		d.logger.With("err", err).Error("Refreshing targets failed")
107	} else {
108		ch <- []*config.TargetGroup{tg}
109	}
110
111	for {
112		select {
113		case <-ticker.C:
114			tg, err := d.refresh()
115			if err != nil {
116				d.logger.With("err", err).Error("Refreshing targets failed")
117			} else {
118				ch <- []*config.TargetGroup{tg}
119			}
120		case <-ctx.Done():
121			return
122		}
123	}
124}
125
126func (d *Discovery) refresh() (tg *config.TargetGroup, err error) {
127	t0 := time.Now()
128	defer func() {
129		refreshDuration.Observe(time.Since(t0).Seconds())
130		if err != nil {
131			refreshFailuresCount.Inc()
132		}
133	}()
134
135	var endpoint = fmt.Sprintf("https://%s:%d/v%d/discover", d.sdConfig.Endpoint, d.sdConfig.Port, d.sdConfig.Version)
136	tg = &config.TargetGroup{
137		Source: endpoint,
138	}
139
140	resp, err := d.client.Get(endpoint)
141	if err != nil {
142		return tg, fmt.Errorf("an error occurred when requesting targets from the discovery endpoint. %s", err)
143	}
144
145	defer resp.Body.Close()
146
147	data, err := ioutil.ReadAll(resp.Body)
148	if err != nil {
149		return tg, fmt.Errorf("an error occurred when reading the response body. %s", err)
150	}
151
152	dr := DiscoveryResponse{}
153	err = json.Unmarshal(data, &dr)
154	if err != nil {
155		return tg, fmt.Errorf("an error occurred unmarshaling the disovery response json. %s", err)
156	}
157
158	for _, container := range dr.Containers {
159		labels := model.LabelSet{
160			tritonLabelMachineId:    model.LabelValue(container.VMUUID),
161			tritonLabelMachineAlias: model.LabelValue(container.VMAlias),
162			tritonLabelMachineBrand: model.LabelValue(container.VMBrand),
163			tritonLabelMachineImage: model.LabelValue(container.VMImageUUID),
164			tritonLabelServerId:     model.LabelValue(container.ServerUUID),
165		}
166		addr := fmt.Sprintf("%s.%s:%d", container.VMUUID, d.sdConfig.DNSSuffix, d.sdConfig.Port)
167		labels[model.AddressLabel] = model.LabelValue(addr)
168		tg.Targets = append(tg.Targets, labels)
169	}
170
171	return tg, nil
172}
173