-
Notifications
You must be signed in to change notification settings - Fork 1
/
httpapi_exporter.go
339 lines (308 loc) · 10.1 KB
/
httpapi_exporter.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
package main
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"regexp"
"strings"
"syscall"
"time"
_ "net/http/pprof"
kingpin "github.com/alecthomas/kingpin/v2"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/expfmt"
"github.com/prometheus/common/promslog"
"github.com/prometheus/common/promslog/flag"
"github.com/prometheus/common/version"
"github.com/prometheus/exporter-toolkit/web"
"github.com/prometheus/exporter-toolkit/web/kingpinflag"
)
const (
// Constant values
metricsPublishingPort = ":9321"
exporter_name = "httpapi_exporter"
)
var (
// listenAddress = kingpin.Flag("web.listen-address", "The address to listen on for HTTP requests.").Default(metricsPublishingPort).String()
metricsPath = kingpin.Flag("web.telemetry-path", "Path under which to expose collector's internal metrics.").Default("/metrics").String()
configFile = kingpin.Flag("config.file", "Exporter configuration file.").Short('c').Default("config/config.yml").String()
//debug_flag = kingpin.Flag("debug", "debug connection checks.").Short('d').Default("false").Bool()
dry_run = kingpin.Flag("dry-run", "Only check exporter configuration file and exit.").Short('n').Default("false").Bool()
// alsologtostderr = kingpin.Flag("alsologtostderr", "log to standard error as well as files.").Default("true").Bool()
target_name = kingpin.Flag("target", "In dry-run mode specify the target name, else ignored.").Short('t').String()
model_name = kingpin.Flag("model", "In dry-run mode specify the model name to build the dynamic target, else ignored.").Default("default").Short('m').String()
auth_key = kingpin.Flag("auth.key", "In dry-run mode specify the auth_key to use, else ignored.").Short('a').String()
collector_name = kingpin.Flag("collector", "Specify the collector name restriction to collect, replace the collector_names set for each target.").Short('o').String()
toolkitFlags = kingpinflag.AddFlags(kingpin.CommandLine, metricsPublishingPort)
logConfig = promslog.Config{Style: promslog.GoKitStyle}
)
const (
OpEgals = 1
OpMatch = 2
)
type route struct {
path string
regex *regexp.Regexp
handler http.HandlerFunc
}
type ctxKey struct {
}
type ctxValue struct {
path string
}
func newRoute(op int, path string, handler http.HandlerFunc) *route {
if op == OpEgals {
return &route{path, nil, handler}
} else if op == OpMatch {
return &route{"", regexp.MustCompile("^" + path + "$"), handler}
} else {
return nil
}
}
func BuildHandler(exporter Exporter, actionCh chan<- actionMsg) http.Handler {
var routes = []*route{
newRoute(OpEgals, "/", HomeHandlerFunc(*metricsPath, exporter)),
newRoute(OpEgals, "/config", ConfigHandlerFunc(*metricsPath, exporter)),
newRoute(OpEgals, "/health", HealthHandlerfunc(*metricsPath, exporter)),
newRoute(OpEgals, "/httpapi_exporter_metrics", func(w http.ResponseWriter, r *http.Request) { promhttp.Handler().ServeHTTP(w, r) }),
newRoute(OpEgals, "/reload", ReloadHandlerFunc(*metricsPath, exporter, actionCh)),
newRoute(OpMatch, "/loglevel(?:/(.*))?", LogLevelHandlerFunc(*metricsPath, exporter, actionCh, "")),
newRoute(OpEgals, "/status", StatusHandlerFunc(*metricsPath, exporter)),
newRoute(OpEgals, "/targets", TargetsHandlerFunc(*metricsPath, exporter)),
newRoute(OpEgals, *metricsPath, func(w http.ResponseWriter, r *http.Request) { ExporterHandlerFor(exporter).ServeHTTP(w, r) }),
// Expose exporter metrics separately, for debugging purposes.
// pprof handle
newRoute(OpMatch, "/debug/.+", http.DefaultServeMux.ServeHTTP),
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
for _, route := range routes {
if route == nil {
continue
}
if route.regex != nil {
matches := route.regex.FindStringSubmatch(req.URL.Path)
if len(matches) > 0 {
var path string
if len(matches) > 1 {
path = matches[1]
}
ctxval := &ctxValue{
path: path,
}
ctx := context.WithValue(req.Context(), ctxKey{}, ctxval)
route.handler(w, req.WithContext(ctx))
return
}
} else if req.URL.Path == route.path {
route.handler(w, req)
return
}
}
err := errors.New("not found")
HandleError(http.StatusNotFound, err, *metricsPath, exporter, w, req)
})
}
type actionMsg struct {
actiontype int
logLevel string
retCh chan error
}
const (
ACTION_RELOAD = iota
ACTION_LOGLEVEL = iota
)
// ReloadHandlerFunc is the HTTP handler for the POST reload entry point (`/reload`).
func ReloadHandlerFunc(metricsPath string, exporter Exporter, reloadCh chan<- actionMsg) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
exporter.Logger().Info(
"received invalid method on /reload", "client", r.RemoteAddr)
HandleError(http.StatusMethodNotAllowed, errors.New("this endpoint requires a POST request"), metricsPath, exporter, w, r)
return
}
exporter.Logger().Info(
"received /reload from %s", "client", r.RemoteAddr)
msg := actionMsg{
actiontype: ACTION_RELOAD,
retCh: make(chan error),
}
reloadCh <- msg
if err := <-msg.retCh; err != nil {
HandleError(http.StatusInternalServerError, err, metricsPath, exporter, w, r)
}
http.Error(w, "OK reload asked.", http.StatusOK)
}
}
func main() {
flag.AddFlags(kingpin.CommandLine, &logConfig)
kingpin.Version(version.Print(exporter_name)).VersionFlag.Short('V')
kingpin.HelpFlag.Short('h')
kingpin.Parse()
logger := promslog.New(&logConfig)
logger.Info(fmt.Sprintf("Starting %s", exporter_name), "version", version.Info())
logger.Info("Build context", "build_context", version.BuildContext())
exporter, err := NewExporter(*configFile, logger, *collector_name)
if err != nil {
logger.Error(fmt.Sprintf("Error creating exporter: %s", err))
os.Exit(1)
}
exporter.SetLogLevel(logConfig.Level.String())
if *dry_run {
logger.Info("configuration OK.")
// get the target if defined
var (
err error
t Target
tmp_t *TargetConfig
)
if *target_name != "" {
*target_name = strings.TrimSpace(*target_name)
t, err = exporter.FindTarget(*target_name)
if err == ErrTargetNotFound {
err = nil
if *model_name != "" {
*model_name = strings.TrimSpace(*model_name)
t_def, err := exporter.FindTarget(*model_name)
if err != nil {
err := fmt.Errorf("Target model '%s' not found: %s", *model_name, err)
logger.Error(err.Error())
os.Exit(1)
}
if tmp_t, err = t_def.Config().Clone(*target_name, ""); err != nil {
err := fmt.Errorf("invalid url set for remote_target '%s' %s", *target_name, err)
logger.Error(err.Error())
os.Exit(1)
}
t, err = exporter.AddTarget(tmp_t)
if err != nil {
err := fmt.Errorf("unable to create temporary target %s", err)
logger.Error(err.Error())
os.Exit(1)
}
exporter.Config().Targets = append(exporter.Config().Targets, tmp_t)
}
}
if err == ErrTargetNotFound {
t, err = exporter.GetFirstTarget()
}
} else {
t, err = exporter.GetFirstTarget()
}
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
if *auth_key != "" {
t.SetSymbol("auth_key", *auth_key)
}
logger.Info(fmt.Sprintf("try to collect target %s.", t.Name()))
timeout := time.Duration(0)
configTimeout := time.Duration(exporter.Config().Globals.ScrapeTimeout)
// If the configured scrape timeout is more restrictive, use that instead.
if configTimeout > 0 && (timeout <= 0 || configTimeout < timeout) {
timeout = configTimeout
}
var ctx context.Context
var cancel context.CancelFunc
if timeout <= 0 {
ctx = context.Background()
cancel = func() {}
} else {
ctx, cancel = context.WithTimeout(context.Background(), timeout)
}
defer cancel()
gatherer := prometheus.Gatherers{exporter.WithContext(ctx, t)}
mfs, err := gatherer.Gather()
if err != nil {
logger.Error(fmt.Sprintf("Error gathering metrics: %v", err))
if len(mfs) == 0 {
os.Exit(1)
}
} else {
logger.Info("collect is OK. Dumping result to stdout.")
}
//dump metric to stdout
enc := expfmt.NewEncoder(os.Stdout, `text/plain; version=`+expfmt.TextVersion+`; charset=utf-8`)
for _, mf := range mfs {
err := enc.Encode(mf)
if err != nil {
logger.Error(err.Error())
break
}
}
if closer, ok := enc.(expfmt.Closer); ok {
// This in particular takes care of the final "# EOF\n" line for OpenMetrics.
closer.Close()
}
logger.Info("dry-run is over. Exiting.")
os.Exit(0)
}
exporter.SetStartTime(time.Now())
exporter.SetReloadTime(time.Now())
user2 := make(chan os.Signal, 1)
init_sigusr2(user2)
hup := make(chan os.Signal, 1)
signal.Notify(hup, syscall.SIGHUP)
actionCh := make(chan actionMsg)
go func() {
for {
select {
case <-user2:
exporter.IncreaseLogLevel("")
case <-hup:
logger.Info("file reloading.")
if err := exporter.ReloadConfig(); err != nil {
logger.Error(fmt.Sprintf("reload err: %s.", err))
} else {
logger.Info("file reloaded.")
}
case action := <-actionCh:
switch action.actiontype {
case ACTION_RELOAD:
logger.Info("file reloading received.")
if err := exporter.ReloadConfig(); err != nil {
logger.Error(fmt.Sprintf("reload err: %s.", err))
action.retCh <- err
} else {
logger.Info("file reloaded.")
action.retCh <- nil
}
case ACTION_LOGLEVEL:
if action.logLevel == "" {
logger.Info("increase loglevel received.")
} else {
logger.Info("set loglevel received.")
}
exporter.IncreaseLogLevel(action.logLevel)
action.retCh <- errors.New(exporter.GetLogLevel())
}
}
}
}()
srvc := make(chan struct{})
term := make(chan os.Signal, 1)
signal.Notify(term, os.Interrupt, syscall.SIGTERM)
go func() {
// Setup and start webserver.
server := &http.Server{
Handler: BuildHandler(exporter, actionCh),
}
if err := web.ListenAndServe(server, toolkitFlags, logger); err != nil {
logger.Error(err.Error())
os.Exit(1)
}
}()
for {
select {
case <-term:
logger.Info("Received SIGTERM, exiting gracefully...")
os.Exit(0)
case <-srvc:
os.Exit(1)
}
}
}