Skip to content

Commit 2818245

Browse files
committed
First working version.
1 parent 9cd72ce commit 2818245

File tree

127 files changed

+25777
-248
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

127 files changed

+25777
-248
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/.project
22
/.settings
33
/sql_exporter
4+
/sql_exporter.yml

cmd/sql_exporter/content.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"html/template"
6+
"net/http"
7+
8+
"github.com/alin-sinpalean/sql_exporter"
9+
)
10+
11+
const (
12+
docsUrl = "https://github.com/alin-sinpalean/sql_exporter"
13+
templates = `
14+
{{ define "page" -}}
15+
<html>
16+
<head>
17+
<title>Prometheus SQL Exporter</title>
18+
<style type="text/css">
19+
body { margin: 0; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.42857143; color: #333; background-color: #fff; }
20+
.navbar { display: flex; background-color: #222; margin: 0; border-width: 0 0 1px; border-style: solid; border-color: #080808; }
21+
.navbar > * { margin: 0; padding: 15px; }
22+
.navbar * { line-height: 20px; color: #9d9d9d; }
23+
.navbar a { text-decoration: none; }
24+
.navbar a:hover, .navbar a:focus { color: #fff; }
25+
.navbar-header { font-size: 18px; }
26+
body > * { margin: 15px; padding: 0; }
27+
pre { padding: 10px; font-size: 13px; background-color: #f5f5f5; border: 1px solid #ccc; }
28+
h1, h2 { font-weight: 500; }
29+
a { color: #337ab7; }
30+
a:hover, a:focus { color: #23527c; }
31+
</style>
32+
</head>
33+
<body>
34+
<div class="navbar">
35+
<div class="navbar-header"><a href="/">Prometheus SQL Exporter</a></div>
36+
<div><a href="{{ .MetricsPath }}">Metrics</a></div>
37+
<div><a href="/config">Configuration</a></div>
38+
<div><a href="{{ .DocsUrl }}">Help</a></div>
39+
</div>
40+
{{template "content" .}}
41+
</body>
42+
</html>
43+
{{- end }}
44+
45+
{{ define "content.home" -}}
46+
<p>This is a <a href="{{ .DocsUrl }}">Prometheus SQL Exporter</a> instance.
47+
You are probably looking for its <a href="{{ .MetricsPath }}">metrics</a> handler.</p>
48+
{{- end }}
49+
50+
{{ define "content.config" -}}
51+
<h2>Configuration</h2>
52+
<pre>{{ .Config }}</pre>
53+
{{- end }}
54+
55+
{{ define "content.error" -}}
56+
<h2>Error</h2>
57+
<pre>{{ .Err }}</pre>
58+
{{- end }}
59+
`
60+
)
61+
62+
type tdata struct {
63+
MetricsPath string
64+
DocsUrl string
65+
66+
// `/config` only
67+
Config string
68+
69+
// `/error` only
70+
Err error
71+
}
72+
73+
var (
74+
allTemplates = template.Must(template.New("").Parse(templates))
75+
homeTemplate = pageTemplate("home")
76+
configTemplate = pageTemplate("config")
77+
errorTemplate = pageTemplate("error")
78+
)
79+
80+
func pageTemplate(name string) *template.Template {
81+
pageTemplate := fmt.Sprintf(`{{define "content"}}{{template "content.%s" .}}{{end}}{{template "page" .}}`, name)
82+
return template.Must(template.Must(allTemplates.Clone()).Parse(pageTemplate))
83+
}
84+
85+
func HomeHandlerFunc(metricsPath string) func(http.ResponseWriter, *http.Request) {
86+
return func(w http.ResponseWriter, r *http.Request) {
87+
homeTemplate.Execute(w, &tdata{
88+
MetricsPath: metricsPath,
89+
DocsUrl: docsUrl,
90+
})
91+
}
92+
}
93+
94+
func ConfigHandlerFunc(metricsPath string, exporter sql_exporter.Exporter) func(http.ResponseWriter, *http.Request) {
95+
return func(w http.ResponseWriter, r *http.Request) {
96+
config, err := exporter.Config().YAML()
97+
if err != nil {
98+
HandleError(err, metricsPath, w, r)
99+
return
100+
}
101+
configTemplate.Execute(w, &tdata{
102+
MetricsPath: metricsPath,
103+
DocsUrl: docsUrl,
104+
Config: string(config),
105+
})
106+
}
107+
}
108+
109+
func HandleError(err error, metricsPath string, w http.ResponseWriter, r *http.Request) {
110+
w.WriteHeader(http.StatusInternalServerError)
111+
errorTemplate.Execute(w, &tdata{
112+
MetricsPath: metricsPath,
113+
DocsUrl: docsUrl,
114+
Err: err,
115+
})
116+
}

cmd/sql_exporter/main.go

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func main() {
3131
alsoLogToStderr.Value.Set("true")
3232
}
3333
// Override the config.file default with the CONFIG environment variable, if set. If the flag is explicitly set, it
34-
// will override both.
34+
// will end up overriding either.
3535
if envConfigFile := os.Getenv("CONFIG"); envConfigFile != "" {
3636
*configFile = envConfigFile
3737
}
@@ -54,19 +54,11 @@ func main() {
5454
ErrorLog: LogFunc(log.Error),
5555
ErrorHandling: promhttp.ContinueOnError,
5656
}
57-
// Expose metrics from our own exporter, which merges SQL target metrics with those from the default gatherer.
57+
// Expose metrics from our own Exporter, which merges SQL target metrics with those from the default gatherer.
5858
http.Handle(*metricsPath, promhttp.HandlerFor(exporter, opts))
5959
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { http.Error(w, "OK", http.StatusOK) })
60-
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
61-
w.Write([]byte(`<html>
62-
<head><title>SQL Exporter</title></head>
63-
<body>
64-
<h1>SQL Exporter</h1>
65-
<p><a href="` + *metricsPath + `">Metrics</a></p>
66-
</body>
67-
</html>
68-
`))
69-
})
60+
http.HandleFunc("/", HomeHandlerFunc(*metricsPath))
61+
http.HandleFunc("/config", ConfigHandlerFunc(*metricsPath, exporter))
7062

7163
log.Infof("Listening on %s", *listenAddress)
7264
log.Fatal(http.ListenAndServe(*listenAddress, nil))

collector.go

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,76 +9,83 @@ import (
99
dto "github.com/prometheus/client_model/go"
1010
)
1111

12-
// Collector is a self-contained group of SQL queries and metrics to collect from a specific database. It is
13-
// conceptually similar to a prometheus.Collector, but doesn't implement it because it requires a context to run in.
12+
// Collector is a self-contained group of SQL queries and metric families to collect from a specific database. It is
13+
// conceptually similar to a prometheus.Collector.
1414
type Collector interface {
1515
// Collect is the equivalent of prometheus.Collector.Collect() but takes a context to run in and a database to run on.
16-
Collect(context.Context, *sql.DB, chan<- MetricValue)
16+
Collect(context.Context, *sql.DB, chan<- Metric)
1717
}
1818

1919
// collector implements Collector. It wraps a collection of queries, metrics and the database to collect them from.
2020
type collector struct {
2121
config *CollectorConfig
2222
queries []*Query
23-
metrics []*Metric
2423
}
2524

2625
// NewCollector returns a new Collector with the given configuration and database. The metrics it creates will all have
2726
// the provided const labels applied.
2827
func NewCollector(cc *CollectorConfig, constLabels []*dto.LabelPair) (Collector, error) {
29-
metrics := make([]*Metric, 0, len(cc.Metrics))
30-
queries := make([]*Query, 0, len(cc.Metrics))
28+
// Maps each query to the list of metric families it populates.
29+
queryMFs := make(map[*QueryConfig][]*MetricFamily, len(cc.Metrics))
3130

31+
// Instantiate metric families.
3232
for _, mc := range cc.Metrics {
33-
m, err := NewMetric(mc, constLabels)
33+
mf, err := NewMetricFamily(mc, constLabels)
3434
if err != nil {
3535
return nil, errors.Wrapf(err, "error in metric %q defined by collector %q", mc.Name, cc.Name)
3636
}
37-
metrics = append(metrics, m)
37+
mfs, found := queryMFs[mc.query]
38+
if !found {
39+
mfs = make([]*MetricFamily, 0, 2)
40+
}
41+
queryMFs[mc.query] = append(mfs, mf)
42+
}
3843

39-
q, err := NewQuery(mc.Query, m)
44+
// Instantiate queries.
45+
queries := make([]*Query, 0, len(cc.Metrics))
46+
for qc, mfs := range queryMFs {
47+
q, err := NewQuery(qc, mfs...)
4048
if err != nil {
41-
return nil, errors.Wrapf(err, "error in query defined by collector %q: %s", cc.Name, mc.Query)
49+
return nil, errors.Wrapf(err, "error in query %q defined by collector %q", qc.Name, cc.Name)
4250
}
4351
queries = append(queries, q)
4452
}
4553

4654
c := collector{
4755
config: cc,
4856
queries: queries,
49-
metrics: metrics,
5057
}
5158
return &c, nil
5259
}
5360

5461
// Collect implements Collector.
55-
func (c *collector) Collect(ctx context.Context, conn *sql.DB, ch chan<- MetricValue) {
62+
func (c *collector) Collect(ctx context.Context, conn *sql.DB, ch chan<- Metric) {
5663
for _, q := range c.queries {
5764
if ctx.Err() != nil {
58-
ch <- NewInvalidMetric(ctx.Err())
65+
ch <- NewInvalidMetric(c.String(), ctx.Err())
5966
return
6067
}
6168
rows, err := q.Run(ctx, conn)
6269
if err != nil {
6370
// TODO: increment an error counter
64-
ch <- NewInvalidMetric(err)
71+
ch <- NewInvalidMetric(c.String(), err)
6572
continue
6673
}
6774
defer rows.Close()
6875

6976
for rows.Next() {
7077
row, err := q.ScanRow(rows)
7178
if err != nil {
72-
ch <- NewInvalidMetric(errors.Wrapf(err, "error while scanning row in collector %q", c.config.Name))
79+
ch <- NewInvalidMetric(fmt.Sprintf("error scanning row in collector %q", c.config.Name), err)
7380
continue
7481
}
75-
for _, m := range q.metrics {
76-
m.Collect(row, ch)
82+
for _, mf := range q.metricFamilies {
83+
mf.Collect(row, ch)
7784
}
7885
}
7986
rows.Close()
8087
if err = rows.Err(); err != nil {
81-
ch <- NewInvalidMetric(err)
88+
ch <- NewInvalidMetric(c.String(), err)
8289
}
8390
}
8491
}

config.go

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func LoadConfig(path string) (*Config, error) {
2929

3030
// Config is a collection of jobs and collectors.
3131
type Config struct {
32-
Globals GlobalConfig `yaml:"global,omitempty"`
32+
Globals GlobalConfig `yaml:"global"`
3333
Jobs []*JobConfig `yaml:"jobs"`
3434
Collectors []*CollectorConfig `yaml:"collectors"`
3535

@@ -74,9 +74,14 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
7474
return checkOverflow(c.XXX, "config")
7575
}
7676

77+
// YAML marshals the config into YAML format.
78+
func (c *Config) YAML() ([]byte, error) {
79+
return yaml.Marshal(c)
80+
}
81+
7782
// GlobalConfig contains globally applicable defaults.
7883
type GlobalConfig struct {
79-
MinInterval model.Duration `yaml:"min_interval,omitempty"` // minimum interval between query executions, default is 0
84+
MinInterval model.Duration `yaml:"min_interval"` // minimum interval between query executions, default is 0
8085

8186
// Catches all undefined fields and must be empty after parsing.
8287
XXX map[string]interface{} `yaml:",inline" json:"-"`
@@ -202,6 +207,17 @@ func (s *StaticConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
202207
return checkOverflow(s.XXX, "static_config")
203208
}
204209

210+
func (s *StaticConfig) MarshalYAML() (interface{}, error) {
211+
result := StaticConfig{
212+
Targets: make(map[string]string, len(s.Targets)),
213+
Labels: s.Labels,
214+
}
215+
for tname, _ := range s.Targets {
216+
result.Targets[tname] = "<secret>"
217+
}
218+
return result, nil
219+
}
220+
205221
//
206222
// Collectors
207223
//
@@ -211,6 +227,7 @@ type CollectorConfig struct {
211227
Name string `yaml:"collector_name"` // name of this collector
212228
MinInterval model.Duration `yaml:"min_interval,omitempty"` // minimum interval between query executions
213229
Metrics []*MetricConfig `yaml:"metrics"` // metrics/queries defined by this collector
230+
Queries []*QueryConfig `yaml:"queries,omitempty"` // named queries defined by this collector
214231

215232
// Catches all undefined fields and must be empty after parsing.
216233
XXX map[string]interface{} `yaml:",inline" json:"-"`
@@ -230,6 +247,28 @@ func (c *CollectorConfig) UnmarshalYAML(unmarshal func(interface{}) error) error
230247
return fmt.Errorf("no metrics defined for collector %q", c.Name)
231248
}
232249

250+
// Set metric.query for all metrics: resolve query references (if any) and generate QueryConfigs for literal queries.
251+
queries := make(map[string]*QueryConfig, len(c.Queries))
252+
for _, query := range c.Queries {
253+
queries[query.Name] = query
254+
}
255+
for _, metric := range c.Metrics {
256+
if metric.QueryRef != "" {
257+
query, found := queries[metric.QueryRef]
258+
if !found {
259+
return fmt.Errorf("unresolved query_ref %q in metric %q of collector %q", metric.QueryRef, metric.Name, c.Name)
260+
}
261+
metric.query = query
262+
query.metrics = append(query.metrics, metric)
263+
} else {
264+
// For literal queries generate a QueryConfig with a name based off collector and metric name.
265+
metric.query = &QueryConfig{
266+
Name: fmt.Sprintf("%s.%s", c.Name, metric.Name),
267+
Query: metric.Query,
268+
}
269+
}
270+
}
271+
233272
return checkOverflow(c.XXX, "collector")
234273
}
235274

@@ -245,7 +284,8 @@ type MetricConfig struct {
245284
Query string `yaml:"query,omitempty"` // a literal query
246285
QueryRef string `yaml:"query_ref,omitempty"` // references a query in the query map
247286

248-
valueType prometheus.ValueType
287+
valueType prometheus.ValueType // TypeString converted to prometheus.ValueType
288+
query *QueryConfig // QueryConfig resolved from QueryRef or generated from Query
249289

250290
// Catches all undefined fields and must be empty after parsing.
251291
XXX map[string]interface{} `yaml:",inline" json:"-"`
@@ -268,8 +308,8 @@ func (m *MetricConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
268308
if m.Help == "" {
269309
return fmt.Errorf("missing help for metric %q", m.Name)
270310
}
271-
if m.Query == "" {
272-
return fmt.Errorf("missing query for metric %q", m.Name)
311+
if (m.Query == "") == (m.QueryRef == "") {
312+
return fmt.Errorf("exactly one of query and query_ref should be specified for metric %q", m.Name)
273313
}
274314

275315
switch strings.ToLower(m.TypeString) {
@@ -309,6 +349,37 @@ func (m *MetricConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
309349
return checkOverflow(m.XXX, "metric")
310350
}
311351

352+
// QueryConfig defines a named query, to be referenced by one or multiple metrics.
353+
type QueryConfig struct {
354+
Name string `yaml:"query_name"` // the query name, to be referenced via `query_ref`
355+
Query string `yaml:"query"` // the named query
356+
357+
metrics []*MetricConfig // metrics referencing this query
358+
359+
// Catches all undefined fields and must be empty after parsing.
360+
XXX map[string]interface{} `yaml:",inline" json:"-"`
361+
}
362+
363+
// UnmarshalYAML implements the yaml.Unmarshaler interface for QueryConfig.
364+
func (q *QueryConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
365+
type plain QueryConfig
366+
if err := unmarshal((*plain)(q)); err != nil {
367+
return err
368+
}
369+
370+
// Check required fields
371+
if q.Name == "" {
372+
return fmt.Errorf("missing name for query %+v", q)
373+
}
374+
if q.Query == "" {
375+
return fmt.Errorf("missing query literal for query %q", q.Name)
376+
}
377+
378+
q.metrics = make([]*MetricConfig, 0, 2)
379+
380+
return checkOverflow(q.XXX, "metric")
381+
}
382+
312383
func checkLabel(label string, ctx ...string) error {
313384
if label == "" {
314385
return fmt.Errorf("empty label defined in %s", strings.Join(ctx, " "))

0 commit comments

Comments
 (0)