json_exporter/exporter/util.go

207 lines
5.6 KiB
Go

// Copyright 2020 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 exporter
import (
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"math"
"net/http"
"net/url"
"strconv"
"strings"
"text/template"
"github.com/Masterminds/sprig/v3"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus-community/json_exporter/config"
"github.com/prometheus/client_golang/prometheus"
pconfig "github.com/prometheus/common/config"
)
func MakeMetricName(parts ...string) string {
return strings.Join(parts, "_")
}
func SanitizeValue(s string) (float64, error) {
var err error
var value float64
var resultErr string
if value, err = strconv.ParseFloat(s, 64); err == nil {
return value, nil
}
resultErr = fmt.Sprintf("%s", err)
if boolValue, err := strconv.ParseBool(s); err == nil {
if boolValue {
return 1.0, nil
}
return 0.0, nil
}
resultErr = resultErr + "; " + fmt.Sprintf("%s", err)
if s == "<nil>" {
return math.NaN(), nil
}
return value, fmt.Errorf(resultErr)
}
func CreateMetricsList(c config.Config) ([]JSONMetric, error) {
var metrics []JSONMetric
for _, metric := range c.Metrics {
switch metric.Type {
case config.ValueScrape:
var variableLabels, variableLabelsValues []string
for k, v := range metric.Labels {
variableLabels = append(variableLabels, k)
variableLabelsValues = append(variableLabelsValues, v)
}
jsonMetric := JSONMetric{
Desc: prometheus.NewDesc(
metric.Name,
metric.Help,
variableLabels,
nil,
),
KeyJSONPath: metric.Path,
LabelsJSONPaths: variableLabelsValues,
}
metrics = append(metrics, jsonMetric)
case config.ObjectScrape:
for subName, valuePath := range metric.Values {
name := MakeMetricName(metric.Name, subName)
var variableLabels, variableLabelsValues []string
for k, v := range metric.Labels {
variableLabels = append(variableLabels, k)
variableLabelsValues = append(variableLabelsValues, v)
}
jsonMetric := JSONMetric{
Desc: prometheus.NewDesc(
name,
metric.Help,
variableLabels,
nil,
),
KeyJSONPath: metric.Path,
ValueJSONPath: valuePath,
LabelsJSONPaths: variableLabelsValues,
}
metrics = append(metrics, jsonMetric)
}
default:
return nil, fmt.Errorf("Unknown metric type: '%s', for metric: '%s'", metric.Type, metric.Name)
}
}
return metrics, nil
}
type JSONFetcher struct {
config config.Config
ctx context.Context
logger log.Logger
method string
body io.Reader
}
func NewJSONFetcher(ctx context.Context, logger log.Logger, c config.Config, tplValues url.Values) *JSONFetcher {
method, body := renderBody(logger, c.Body, tplValues)
return &JSONFetcher{
config: c,
ctx: ctx,
logger: logger,
method: method,
body: body,
}
}
func (f *JSONFetcher) FetchJSON(endpoint string) ([]byte, error) {
httpClientConfig := f.config.HTTPClientConfig
client, err := pconfig.NewClientFromConfig(httpClientConfig, "fetch_json", pconfig.WithKeepAlivesDisabled(), pconfig.WithHTTP2Disabled())
if err != nil {
level.Error(f.logger).Log("msg", "Error generating HTTP client", "err", err)
return nil, err
}
var req *http.Request
req, err = http.NewRequest(f.method, endpoint, f.body)
req = req.WithContext(f.ctx)
if err != nil {
level.Error(f.logger).Log("msg", "Failed to create request", "err", err)
return nil, err
}
for key, value := range f.config.Headers {
req.Header.Add(key, value)
}
if req.Header.Get("Accept") == "" {
req.Header.Add("Accept", "application/json")
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer func() {
if _, err := io.Copy(ioutil.Discard, resp.Body); err != nil {
level.Error(f.logger).Log("msg", "Failed to discard body", "err", err)
}
resp.Body.Close()
}()
if resp.StatusCode/100 != 2 {
return nil, errors.New(resp.Status)
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return data, nil
}
// Use the configured template to render the body if enabled
// Do not treat template errors as fatal, on such errors just log them
// and continue with static body content
func renderBody(logger log.Logger, body config.Body, tplValues url.Values) (method string, br io.Reader) {
method = "POST"
if body.Content == "" {
return "GET", nil
}
br = strings.NewReader(body.Content)
if body.Templatize {
tpl, err := template.New("base").Funcs(sprig.TxtFuncMap()).Parse(body.Content)
if err != nil {
level.Error(logger).Log("msg", "Failed to create a new template from body content", "err", err, "content", body.Content)
return
}
tpl = tpl.Option("missingkey=zero")
var b strings.Builder
if err := tpl.Execute(&b, tplValues); err != nil {
level.Error(logger).Log("msg", "Failed to render template with values", "err", err, "tempalte", body.Content)
// `tplValues` can contain sensitive values, so log it only when in debug mode
level.Debug(logger).Log("msg", "Failed to render template with values", "err", err, "tempalte", body.Content, "values", tplValues, "rendered_body", b.String())
return
}
br = strings.NewReader(b.String())
}
return
}