// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you 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 json import ( "encoding/json" "io/ioutil" "net/http" "strconv" "strings" "github.com/elastic/beats/libbeat/common" "github.com/elastic/beats/metricbeat/helper" "github.com/elastic/beats/metricbeat/mb" "github.com/elastic/beats/metricbeat/mb/parse" ) // init registers the MetricSet with the central registry. // The New method will be called after the setup of the module and before starting to fetch data func init() { if err := mb.Registry.AddMetricSet("http", "json", New, hostParser); err != nil { panic(err) } } const ( // defaultScheme is the default scheme to use when it is not specified in the host config. defaultScheme = "http" // defaultPath is the dto use when it is not specified in the host config. defaultPath = "" ) var ( hostParser = parse.URLHostParserBuilder{ DefaultScheme: defaultScheme, PathConfigKey: "path", DefaultPath: defaultPath, }.Build() ) // MetricSet type defines all fields of the MetricSet // As a minimum it must inherit the mb.BaseMetricSet fields, but can be extended with // additional entries. These variables can be used to persist data or configuration between // multiple fetch calls. type MetricSet struct { mb.BaseMetricSet namespace string http *helper.HTTP method string body string requestEnabled bool responseEnabled bool jsonIsArray bool deDotEnabled bool } // New create a new instance of the MetricSet // Part of new is also setting up the configuration by processing additional // configuration entries if needed. func New(base mb.BaseMetricSet) (mb.MetricSet, error) { config := struct { Namespace string `config:"namespace" validate:"required"` Method string `config:"method"` Body string `config:"body"` RequestEnabled bool `config:"request.enabled"` ResponseEnabled bool `config:"response.enabled"` JSONIsArray bool `config:"json.is_array"` DeDotEnabled bool `config:"dedot.enabled"` }{ Method: "GET", Body: "", RequestEnabled: false, ResponseEnabled: false, JSONIsArray: false, DeDotEnabled: false, } if err := base.Module().UnpackConfig(&config); err != nil { return nil, err } http, err := helper.NewHTTP(base) if err != nil { return nil, err } http.SetMethod(config.Method) http.SetBody([]byte(config.Body)) return &MetricSet{ BaseMetricSet: base, namespace: config.Namespace, method: config.Method, body: config.Body, http: http, requestEnabled: config.RequestEnabled, responseEnabled: config.ResponseEnabled, jsonIsArray: config.JSONIsArray, deDotEnabled: config.DeDotEnabled, }, nil } func (m *MetricSet) processBody(response *http.Response, jsonBody interface{}) common.MapStr { var event common.MapStr if m.deDotEnabled { event = common.DeDotJSON(jsonBody).(common.MapStr) } else { event = jsonBody.(common.MapStr) } if m.requestEnabled { event[mb.ModuleDataKey] = common.MapStr{ "request": common.MapStr{ "headers": m.getHeaders(response.Request.Header), "method": response.Request.Method, "body": m.body, }, } } if m.responseEnabled { phrase := strings.TrimPrefix(response.Status, strconv.Itoa(response.StatusCode)+" ") event[mb.ModuleDataKey] = common.MapStr{ "response": common.MapStr{ "code": response.StatusCode, "phrase": phrase, "headers": m.getHeaders(response.Header), }, } } // Set dynamic namespace event["_namespace"] = m.namespace return event } // Fetch methods implements the data gathering and data conversion to the right format // It returns the event which is then forward to the output. In case of an error, a // descriptive error must be returned. func (m *MetricSet) Fetch() ([]common.MapStr, error) { response, err := m.http.FetchResponse() if err != nil { return nil, err } defer response.Body.Close() var jsonBody common.MapStr var jsonBodyArr []common.MapStr var events []common.MapStr body, err := ioutil.ReadAll(response.Body) if err != nil { return nil, err } if m.jsonIsArray { err = json.Unmarshal(body, &jsonBodyArr) if err != nil { return nil, err } for _, obj := range jsonBodyArr { event := m.processBody(response, obj) events = append(events, event) } } else { err = json.Unmarshal(body, &jsonBody) if err != nil { return nil, err } event := m.processBody(response, jsonBody) events = append(events, event) } return events, nil } func (m *MetricSet) getHeaders(header http.Header) map[string]string { headers := make(map[string]string) for k, v := range header { value := "" for _, h := range v { value += h + " ," } value = strings.TrimRight(value, " ,") headers[k] = value } return headers }