From 9e35e3d07198f16542994c85e003527423b7971e Mon Sep 17 00:00:00 2001 From: MGSousa Date: Sun, 26 Nov 2023 22:22:31 +0000 Subject: [PATCH] Base Version --- .gitignore | 1 + .goreleaser.yml | 18 ++++ exporter/exporter.go | 192 ++++++++++++++++++++++++++++++++++++++ exporter/exporter_test.go | 93 ++++++++++++++++++ exporter/http.go | 62 ++++++++++++ exporter/utils.go | 56 +++++++++++ go.mod | 26 ++++++ go.sum | 58 ++++++++++++ main.go | 97 +++++++++++++++++++ 9 files changed, 603 insertions(+) create mode 100644 .gitignore create mode 100644 .goreleaser.yml create mode 100644 exporter/exporter.go create mode 100644 exporter/exporter_test.go create mode 100644 exporter/http.go create mode 100644 exporter/utils.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..849ddff --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..28e6587 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,18 @@ +project_name: prom-exporter + +builds: + - env: + - CGO_ENABLED=0 + main: ./main.go + goos: + - linux + - windows + - darwin + goarch: + - amd64 + ldflags: + - -s -w +archives: + - format: binary +snapshot: + name_template: "{{ .Tag }}-next" diff --git a/exporter/exporter.go b/exporter/exporter.go new file mode 100644 index 0000000..0d68215 --- /dev/null +++ b/exporter/exporter.go @@ -0,0 +1,192 @@ +package exporter + +import ( + "fmt" + "sort" + "strconv" + + "github.com/prometheus/client_golang/prometheus" + log "github.com/sirupsen/logrus" +) + +type ( + Exporter struct { + uri string + name string + version string + metrics Metrics + + data []string + } + + Metrics []struct { + help *prometheus.Desc + value float64 + vtype prometheus.ValueType + } +) + +func NewCollector(name, uri, version string) *Exporter { + return &Exporter{ + name: name, + uri: uri, + version: version, + metrics: Metrics{}, + } +} + +func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { + for _, metric := range e.metrics { + ch <- metric.help + } +} + +func (e *Exporter) Collect(ch chan<- prometheus.Metric) { + e.process(ch) + + for _, metric := range e.metrics { + ch <- prometheus.MustNewConstMetric(metric.help, metric.vtype, metric.value) + } +} + +var ( + // s = []string{"processor_rate_limit_1::dropped::0", "registrar_states::cleanup::0", "registrar_states::update::0", "registrar_writes::success::0", "system_cpu::cores::4", "system_load::1::0.71", "system_load::15::0.47", "system_load::5::0.53", "system_load_norm::1::0.1775", "system_load_norm::15::0.1175", "system_load_norm::5::0.1325"} + vl, future []string + + fqname string + labels prometheus.Labels + mType prometheus.ValueType +) + +func (e *Exporter) process(ch chan<- prometheus.Metric) { + body, err := FetchMetrics(e.uri) + if err != nil { + // set target down on error + ch <- prometheus.MustNewConstMetric( + prometheus.NewDesc(fmt.Sprintf("%s_up", e.name), "Target up", nil, nil), prometheus.GaugeValue, float64(0)) + log.Debugf("Failed getting metrics endpoint of target: %s ", err.Error()) + return + } + + log.Debugln("Service scrapped up") + ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(fmt.Sprintf("%s_up", e.name), "Target Up", nil, nil), prometheus.GaugeValue, float64(1)) + + // inject service version is exists + if e.version != "" { + ch <- prometheus.MustNewConstMetric( + prometheus.NewDesc(fmt.Sprintf("%s_version_info", e.name), fmt.Sprintf("%s service info", e.name), nil, + prometheus.Labels{"version": e.version}), prometheus.GaugeValue, float64(1)) + } + + // parse response body + p := Parser{data: make([]string, 0)} + p.parse(body) + e.data = p.data + + sort.Slice(e.data, func(i, j int) bool { return e.data[i] < e.data[j] }) + + stats := make(Metrics, len(e.data)) + for k, v := range e.data { + vl = split(v, "::") + + i, err := strconv.ParseFloat(vl[2], 64) + if err != nil { + // a string was found + log.Debugln(err) + i = 0 + } + + // check last filed for the specific ValueType + if contains(vl[1], "total") { + mType = prometheus.CounterValue + } else { + if len(vl) > 3 { + mType = prometheus.UntypedValue + } else { + mType = prometheus.GaugeValue + } + } + + // build Full-Qualified Name + e.build(k) + + // setup metrics + stats[k].help = prometheus.NewDesc( + fqname, "", nil, labels) + stats[k].value = float64(i) + stats[k].vtype = mType + } + + e.metrics = stats +} + +func (e *Exporter) build(k int) { + labels = nil + fp := split(vl[0], "_") + + if len(fp) == 2 { + + if k < (len(e.data) - 1) { + future = split(e.data[k+1], "::") + if vl[0] == future[0] { + + fqname = prometheus.BuildFQName( + e.name, fmt.Sprintf("%s", join(fp[0:len(fp)-1], "_")), fp[len(fp)-1]) + labels = prometheus.Labels{"metric": vl[1]} + } else { + future = split(e.data[k-1], "::") + if vl[0] == future[0] { + fqname = prometheus.BuildFQName( + e.name, fmt.Sprintf("%s", join(fp[0:len(fp)-1], "_")), fp[len(fp)-1]) + labels = prometheus.Labels{"metric": vl[1]} + } else { + fqname = prometheus.BuildFQName(e.name, fmt.Sprintf("%s_%s", fp[0], fp[1]), vl[1]) + } + } + } else if k == (len(e.data) - 1) { + future = split(e.data[k-1], "::") + if vl[0] == future[0] { + fqname = prometheus.BuildFQName( + e.name, fmt.Sprintf("%s", join(fp[0:len(fp)-1], "_")), fp[len(fp)-1]) + labels = prometheus.Labels{"metric": vl[1]} + } else { + fqname = prometheus.BuildFQName(e.name, fmt.Sprintf("%s_%s", fp[0], fp[1]), vl[1]) + } + } else { + fqname = prometheus.BuildFQName(e.name, fmt.Sprintf("%s_%s", fp[0], fp[1]), vl[1]) + } + + } else if len(fp) >= 3 { + + if k < (len(e.data) - 1) { + future = split(e.data[k+1], "::") + if vl[0] == future[0] { + + fqname = prometheus.BuildFQName( + e.name, fmt.Sprintf("%s", join(fp[0:len(fp)-1], "_")), fp[len(fp)-1]) + labels = prometheus.Labels{"metric": vl[1]} + } else { + + if k == 0 { + fqname = prometheus.BuildFQName(e.name, fmt.Sprintf("%s_%s", fp[0], fp[1]), vl[1]) + } else { + future = split(e.data[k-1], "::") + if vl[0] == future[0] { + fqname = prometheus.BuildFQName( + e.name, fmt.Sprintf("%s", join(fp[0:len(fp)-1], "_")), fp[len(fp)-1]) + labels = prometheus.Labels{"metric": vl[1]} + } else { + fqname = prometheus.BuildFQName(e.name, fmt.Sprintf("%s_%s", fp[0], fp[1]), vl[1]) + } + } + } + } else if k == (len(e.data) - 1) { + future = split(e.data[k-1], "::") + if vl[0] == future[0] { + labels = prometheus.Labels{"metric": vl[1]} + } + } + fqname = prometheus.BuildFQName( + e.name, fmt.Sprintf("%s", join(fp[0:len(fp)-1], "_")), fp[len(fp)-1]) + } +} diff --git a/exporter/exporter_test.go b/exporter/exporter_test.go new file mode 100644 index 0000000..83d98f8 --- /dev/null +++ b/exporter/exporter_test.go @@ -0,0 +1,93 @@ +package exporter + +import ( + "reflect" + "testing" + + "github.com/prometheus/client_golang/prometheus" +) + +func TestNewCollector(t *testing.T) { + type args struct { + name string + uri string + } + tests := []struct { + name string + args args + want *Exporter + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewCollector(tt.args.name, tt.args.uri); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewCollector() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestExporter_Collect(t *testing.T) { + type fields struct { + uri string + name string + metrics Metrics + } + type args struct { + ch chan<- prometheus.Metric + } + tests := []struct { + name string + fields fields + args args + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Exporter{ + uri: tt.fields.uri, + name: tt.fields.name, + metrics: tt.fields.metrics, + } + e.Collect(tt.args.ch) + }) + } +} + +func TestExporter_process(t *testing.T) { + type fields struct { + uri string + name string + metrics Metrics + } + type args struct { + ch chan<- prometheus.Metric + } + tests := []struct { + name string + fields fields + args args + }{ + { + // TODO: Add test cases. + name: "test", + fields: fields{ + uri: "http://localhost:5066/stats", + name: "test", + metrics: Metrics{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Exporter{ + uri: tt.fields.uri, + name: tt.fields.name, + metrics: tt.fields.metrics, + } + e.process(tt.args.ch) + }) + } +} diff --git a/exporter/http.go b/exporter/http.go new file mode 100644 index 0000000..1ae5b84 --- /dev/null +++ b/exporter/http.go @@ -0,0 +1,62 @@ +package exporter + +import ( + "encoding/json" + "io" + "net/http" + + log "github.com/sirupsen/logrus" +) + +// TODO: customize this +type ServiceStats struct { + Hostname string `json:"hostname"` + Name string `json:"name"` + Version string `json:"version"` +} + +// FetchStats Fetch base endpoint for internal stats data from the specified service +func FetchStats(uri string) *ServiceStats { + body, err := get(uri) + if err != nil { + return nil + } + + serviceStats := ServiceStats{} + if err = json.Unmarshal(body, &serviceStats); err != nil { + log.Error("Could not parse JSON response from target stats", uri) + return nil + } + return &serviceStats +} + +// FetchMetrics Fetch internal metrics from the specified service +func FetchMetrics(uri string) (any, error) { + var raw any + body, err := get(uri) + if err != nil { + return nil, err + } + + if err = json.Unmarshal(body, &raw); err != nil { + log.Error("Could not parse JSON response for target") + return nil, err + } + return raw, nil +} + +func get(uri string) ([]byte, error) { + res, err := http.Get(uri) + if err != nil { + log.Errorf("Could not fetch metrics for endpoint of target: %s", uri) + return nil, err + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + log.Error("Can't read body of response") + return nil, err + } + return body, nil +} diff --git a/exporter/utils.go b/exporter/utils.go new file mode 100644 index 0000000..3108d0b --- /dev/null +++ b/exporter/utils.go @@ -0,0 +1,56 @@ +package exporter + +import ( + "fmt" + "reflect" + "strings" + + log "github.com/sirupsen/logrus" +) + +type Parser struct { + data []string +} + +func (p *Parser) parse(v any, keys ...string) { + var val interface{} + + switch x := getType(v); x { + case reflect.Map: + for k, nv := range v.(map[string]any) { + p.parse(nv, append(keys, k)...) + } + return + case + reflect.Float64, + reflect.Float32, + reflect.Int64, + reflect.Int32: + val = v + case reflect.String: + val = fmt.Sprintf("%s::untyped", v) + default: + log.Error("parsed type is unknown", x) + } + + metric := strings.Join(keys[:len(keys)-1], "_") + + // TODO: change this + p.data = append(p.data, fmt.Sprintf("%s::%s::%v", metric, keys[len(keys)-1], val)) +} + +func getType(t any) reflect.Kind { + return reflect.TypeOf(t).Kind() +} + +func split(s, delim string) []string { + return strings.Split(s, delim) +} + +func join(s []string, delim string) string { + return strings.Join(s, delim) +} + +func contains(s, substr string) bool { + return strings.Contains(s, substr) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..818b3c1 --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module main + +go 1.21.3 + +require ( + github.com/prometheus/client_golang v1.17.0 + github.com/sirupsen/logrus v1.9.3 +) + +require ( + github.com/cweill/gotests v1.6.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/tools v0.15.0 // indirect +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect + github.com/prometheus/common v0.44.0 + github.com/prometheus/procfs v0.11.1 // indirect + golang.org/x/sys v0.14.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..16a36c8 --- /dev/null +++ b/go.sum @@ -0,0 +1,58 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cweill/gotests v1.6.0 h1:KJx+/p4EweijYzqPb4Y/8umDCip1Cv6hEVyOx0mE9W8= +github.com/cweill/gotests v1.6.0/go.mod h1:CaRYbxQZGQOxXDvM9l0XJVV2Tjb2E5H53vq+reR2GrA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= +github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20191109212701-97ad0ed33101/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= +golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..f9dcb7a --- /dev/null +++ b/main.go @@ -0,0 +1,97 @@ +package main + +import ( + "flag" + "fmt" + "main/exporter" + "net/http" + "os" + "strings" + + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/prometheus/common/version" + log "github.com/sirupsen/logrus" +) + +var ( + listenAddress = flag.String("listen-address", ":19100", "Address to listen on for telemetry.") + metricsPath = flag.String("telemetry-path", "/metrics", "Base path under which to expose metrics.") + serviceName = flag.String("service-name", "", "Service name to reference") + serviceUri = flag.String("service-uri", "http://localhost:5066", "HTTP address of the service.") + serviceMetricsPath = flag.String("service-metrics-path", "metrics", "Service path to scrape metrics from.") + debugLevel = flag.Bool("debug", false, "Enable debug mode") +) + +func main() { + flag.Parse() + + name := *serviceName + if strings.Trim(name, " ") == "" { + log.Fatalln("Service name not known! Specify by -service-name SERVICE") + } + + if *debugLevel { + log.SetLevel(log.DebugLevel) + } + + log.Info("Check if target is reachable...") + serviceVersion := checkEndpoint(*serviceUri) + log.Info("Target endpoint is reachable") + + registry := prometheus.NewRegistry() + + // register version metrics + versionMetric := version.NewCollector(name) + registry.MustRegister(versionMetric) + + // register service metrics + exporter := exporter.NewCollector(name, fmt.Sprintf("%s/%s", *serviceUri, *serviceMetricsPath), serviceVersion) + registry.MustRegister(exporter) + + http.Handle(*metricsPath, promhttp.HandlerFor( + registry, + promhttp.HandlerOpts{ + DisableCompression: false, + ErrorHandling: promhttp.ContinueOnError, + }), + ) + log.Println("Starting server....") + + srv := &http.Server{ + Addr: *listenAddress, + ReadHeaderTimeout: 5 * time.Second, + } + if err := srv.ListenAndServe(); err != nil { + log.WithFields(log.Fields{ + "err": err, + }).Errorf("http server quit with error: %v", err) + } +} + +func checkEndpoint(endpoint string) string { + stopCh := make(chan bool) + t := time.NewTicker(2 * time.Second) + + stats := &exporter.ServiceStats{} + +discovery: + for { + select { + case <-t.C: + if stats = exporter.FetchStats(endpoint); stats != nil { + break discovery + } + log.Errorln("base endpoint not available, retrying in 2s") + continue + + case <-stopCh: + os.Exit(0) + } + } + t.Stop() + + return stats.Version +}