diff --git a/README.md b/README.md index c0a216b0..c953221d 100644 --- a/README.md +++ b/README.md @@ -472,6 +472,48 @@ Everything must compile, including mattn driver for oracle. Next build ./... in oracledb-exporter dir, or install it. +## Import into your Golang Application + +The `oracledb_exporter` can also be imported into your Go based applications. The [Grafana Agent](https://github.com/grafana/agent/) uses this pattern to implement the [OracleDB integration](https://grafana.com/docs/grafana-cloud/data-configuration/integrations/integration-reference/integration-oracledb/). Feel free to modify the code to fit your application's use case. + +Here is a small snippet of an example usage of the exporter in code: + +```go + promLogConfig := &promlog.Config{} + // create your own config + logger := promlog.New(promLogConfig) + + // replace with your connection string + connectionString := "oracle://username:password@localhost:1521/orcl.localnet" + oeExporter, err := oe.NewExporter(logger, &oe.Config{ + DSN: connectionString, + MaxIdleConns: 0, + MaxOpenConns: 10, + QueryTimeout: 5, + }) + + if err != nil { + panic(err) + } + + metricChan := make(chan prometheus.Metric, len(oeExporter.DefaultMetrics().Metric)) + oeExporter.Collect(metricChan) + + // alternatively its possible to run scrapes on an interval + // and Collect() calls will only return updated data once + // that intervaled scrape is run + // please note this is a blocking call so feel free to run + // in a separate goroutine + // oeExporter.RunScheduledScrapes(context.Background(), time.Minute) + + for r := range metricChan { + // Write to the client of your choice + // or spin up a promhttp.Server to serve these metrics + r.Write(&dto.Metric{}) + } + +``` + # FAQ/Troubleshooting ## Unable to convert current value to float (metric=par,metri...in.go:285 diff --git a/collector/collector.go b/collector/collector.go new file mode 100644 index 00000000..15cefcd3 --- /dev/null +++ b/collector/collector.go @@ -0,0 +1,590 @@ +package collector + +import ( + "bytes" + "context" + "crypto/sha256" + "database/sql" + "errors" + "fmt" + "hash" + "io" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/BurntSushi/toml" + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/prometheus/client_golang/prometheus" +) + +// Exporter collects Oracle DB metrics. It implements prometheus.Collector. +type Exporter struct { + config *Config + mu *sync.Mutex + metricsToScrape Metrics + scrapeInterval *time.Duration + dsn string + duration, error prometheus.Gauge + totalScrapes prometheus.Counter + scrapeErrors *prometheus.CounterVec + scrapeResults []prometheus.Metric + up prometheus.Gauge + db *sql.DB + logger log.Logger +} + +// Config is the configuration of the exporter +type Config struct { + DSN string + MaxIdleConns int + MaxOpenConns int + CustomMetrics string + QueryTimeout int +} + +// CreateDefaultConfig returns the default configuration of the Exporter +// it is to be of note that the DNS will be empty when +func CreateDefaultConfig() *Config { + return &Config{ + MaxIdleConns: 0, + MaxOpenConns: 10, + CustomMetrics: "", + QueryTimeout: 5, + } +} + +// Metric is an object description +type Metric struct { + Context string + Labels []string + MetricsDesc map[string]string + MetricsType map[string]string + MetricsBuckets map[string]map[string]string + FieldToAppend string + Request string + IgnoreZeroResult bool +} + +// Metrics is a container structure for prometheus metrics +type Metrics struct { + Metric []Metric +} + +var ( + additionalMetrics Metrics + hashMap map[int][]byte + namespace = "oracledb" + exporterName = "exporter" +) + +func maskDsn(dsn string) string { + parts := strings.Split(dsn, "@") + if len(parts) > 1 { + maskedURL := "***@" + parts[1] + return maskedURL + } + return dsn +} + +// NewExporter creates a new Exporter instance +func NewExporter(logger log.Logger, cfg *Config) (*Exporter, error) { + e := &Exporter{ + mu: &sync.Mutex{}, + dsn: cfg.DSN, + duration: prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: exporterName, + Name: "last_scrape_duration_seconds", + Help: "Duration of the last scrape of metrics from Oracle DB.", + }), + totalScrapes: prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: exporterName, + Name: "scrapes_total", + Help: "Total number of times Oracle DB was scraped for metrics.", + }), + scrapeErrors: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: exporterName, + Name: "scrape_errors_total", + Help: "Total number of times an error occured scraping a Oracle database.", + }, []string{"collector"}), + error: prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: exporterName, + Name: "last_scrape_error", + Help: "Whether the last scrape of metrics from Oracle DB resulted in an error (1 for error, 0 for success).", + }), + up: prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "up", + Help: "Whether the Oracle database server is up.", + }), + logger: logger, + config: cfg, + } + e.metricsToScrape = e.DefaultMetrics() + err := e.connect() + return e, err +} + +// Describe describes all the metrics exported by the Oracle DB exporter. +func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { + // We cannot know in advance what metrics the exporter will generate + // So we use the poor man's describe method: Run a collect + // and send the descriptors of all the collected metrics. The problem + // here is that we need to connect to the Oracle DB. If it is currently + // unavailable, the descriptors will be incomplete. Since this is a + // stand-alone exporter and not used as a library within other code + // implementing additional metrics, the worst that can happen is that we + // don't detect inconsistent metrics created by this exporter + // itself. Also, a change in the monitored Oracle instance may change the + // exported metrics during the runtime of the exporter. + + metricCh := make(chan prometheus.Metric) + doneCh := make(chan struct{}) + + go func() { + for m := range metricCh { + ch <- m.Desc() + } + close(doneCh) + }() + + e.Collect(metricCh) + close(metricCh) + <-doneCh +} + +// Collect implements prometheus.Collector. +func (e *Exporter) Collect(ch chan<- prometheus.Metric) { + // they are running scheduled scrapes we should only scrape new data + // on the interval + if e.scrapeInterval != nil && *e.scrapeInterval != 0 { + // read access must be checked + e.mu.Lock() + for _, r := range e.scrapeResults { + ch <- r + } + e.mu.Unlock() + return + } + + // otherwise do a normal scrape per request + e.mu.Lock() // ensure no simultaneous scrapes + defer e.mu.Unlock() + e.scrape(ch) + ch <- e.duration + ch <- e.totalScrapes + ch <- e.error + e.scrapeErrors.Collect(ch) + ch <- e.up +} + +// RunScheduledScrapes is only relevant for users of this package that want to set the scrape on a timer +// rather than letting it be per Collect call +func (e *Exporter) RunScheduledScrapes(ctx context.Context, si time.Duration) { + e.scrapeInterval = &si + ticker := time.NewTicker(si) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + e.mu.Lock() // ensure no simultaneous scrapes + e.scheduledScrape() + e.mu.Unlock() + case <-ctx.Done(): + return + } + } +} + +func (e *Exporter) scheduledScrape() { + metricCh := make(chan prometheus.Metric, 5) + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + e.scrapeResults = []prometheus.Metric{} + for { + scrapeResult, more := <-metricCh + if more { + e.scrapeResults = append(e.scrapeResults, scrapeResult) + continue + } + return + } + }() + e.scrape(metricCh) + + // report metadata metrics + metricCh <- e.duration + metricCh <- e.totalScrapes + metricCh <- e.error + e.scrapeErrors.Collect(metricCh) + metricCh <- e.up + + close(metricCh) + wg.Wait() +} + +func (e *Exporter) scrape(ch chan<- prometheus.Metric) { + e.totalScrapes.Inc() + var err error + defer func(begun time.Time) { + e.duration.Set(time.Since(begun).Seconds()) + if err == nil { + e.error.Set(0) + } else { + e.error.Set(1) + } + }(time.Now()) + + if err = e.db.Ping(); err != nil { + if strings.Contains(err.Error(), "sql: database is closed") { + level.Info(e.logger).Log("Reconnecting to DB") + err = e.connect() + if err != nil { + level.Error(e.logger).Log("Error reconnecting to DB", err) + } + } + } + + if err = e.db.Ping(); err != nil { + level.Error(e.logger).Log("Error pinging oracle:", err) + e.up.Set(0) + return + } + + level.Debug(e.logger).Log("Successfully pinged Oracle database: ", maskDsn(e.dsn)) + e.up.Set(1) + + if e.checkIfMetricsChanged() { + e.reloadMetrics() + } + + wg := sync.WaitGroup{} + + for _, metric := range e.metricsToScrape.Metric { + wg.Add(1) + metric := metric //https://golang.org/doc/faq#closures_and_goroutines + + go func() { + defer wg.Done() + + level.Debug(e.logger).Log("About to scrape metric: ") + level.Debug(e.logger).Log("- Metric MetricsDesc: ", metric.MetricsDesc) + level.Debug(e.logger).Log("- Metric Context: ", metric.Context) + level.Debug(e.logger).Log("- Metric MetricsType: ", metric.MetricsType) + level.Debug(e.logger).Log("- Metric MetricsBuckets: ", metric.MetricsBuckets, "(Ignored unless Histogram type)") + level.Debug(e.logger).Log("- Metric Labels: ", metric.Labels) + level.Debug(e.logger).Log("- Metric FieldToAppend: ", metric.FieldToAppend) + level.Debug(e.logger).Log("- Metric IgnoreZeroResult: ", metric.IgnoreZeroResult) + level.Debug(e.logger).Log("- Metric Request: ", metric.Request) + + if len(metric.Request) == 0 { + level.Error(e.logger).Log("Error scraping for ", metric.MetricsDesc, ". Did you forget to define request in your toml file?") + return + } + + if len(metric.MetricsDesc) == 0 { + level.Error(e.logger).Log("Error scraping for query", metric.Request, ". Did you forget to define metricsdesc in your toml file?") + return + } + + for column, metricType := range metric.MetricsType { + if metricType == "histogram" { + _, ok := metric.MetricsBuckets[column] + if !ok { + level.Error(e.logger).Log("Unable to find MetricsBuckets configuration key for metric. (metric=" + column + ")") + return + } + } + } + + scrapeStart := time.Now() + if err = e.ScrapeMetric(e.db, ch, metric); err != nil { + level.Error(e.logger).Log("Error scraping for", metric.Context, "_", metric.MetricsDesc, time.Since(scrapeStart), ":", err) + e.scrapeErrors.WithLabelValues(metric.Context).Inc() + } else { + level.Debug(e.logger).Log("Successfully scraped metric: ", metric.Context, metric.MetricsDesc, time.Since(scrapeStart)) + } + }() + } + wg.Wait() +} + +func (e *Exporter) connect() error { + level.Debug(e.logger).Log("Launching connection: ", maskDsn(e.dsn)) + db, err := sql.Open("oracle", e.dsn) + if err != nil { + level.Error(e.logger).Log("Error while connecting to", e.dsn) + return err + } + level.Debug(e.logger).Log("set max idle connections to ", e.config.MaxIdleConns) + db.SetMaxIdleConns(e.config.MaxIdleConns) + level.Debug(e.logger).Log("set max open connections to ", e.config.MaxOpenConns) + db.SetMaxOpenConns(e.config.MaxOpenConns) + level.Debug(e.logger).Log("Successfully connected to: ", maskDsn(e.dsn)) + e.db = db + return nil +} + +func (e *Exporter) checkIfMetricsChanged() bool { + for i, _customMetrics := range strings.Split(e.config.CustomMetrics, ",") { + if len(_customMetrics) == 0 { + continue + } + level.Debug(e.logger).Log("Checking modifications in following metrics definition file:", _customMetrics) + h := sha256.New() + if err := hashFile(h, _customMetrics); err != nil { + level.Error(e.logger).Log("Unable to get file hash", err) + return false + } + // If any of files has been changed reload metrics + if !bytes.Equal(hashMap[i], h.Sum(nil)) { + level.Info(e.logger).Log(_customMetrics, "has been changed. Reloading metrics...") + hashMap[i] = h.Sum(nil) + return true + } + } + return false +} + +func hashFile(h hash.Hash, fn string) error { + f, err := os.Open(fn) + if err != nil { + return err + } + defer f.Close() + if _, err := io.Copy(h, f); err != nil { + return err + } + return nil +} + +func (e *Exporter) reloadMetrics() { + // Truncate metricsToScrape + e.metricsToScrape.Metric = []Metric{} + + // Load default metrics + defaultMetrics := e.DefaultMetrics() + e.metricsToScrape.Metric = defaultMetrics.Metric + + // If custom metrics, load it + if strings.Compare(e.config.CustomMetrics, "") != 0 { + for _, _customMetrics := range strings.Split(e.config.CustomMetrics, ",") { + if _, err := toml.DecodeFile(_customMetrics, &additionalMetrics); err != nil { + level.Error(e.logger).Log(err) + panic(errors.New("Error while loading " + _customMetrics)) + } else { + level.Info(e.logger).Log("Successfully loaded custom metrics from: " + _customMetrics) + } + e.metricsToScrape.Metric = append(e.metricsToScrape.Metric, additionalMetrics.Metric...) + } + } else { + level.Debug(e.logger).Log("No custom metrics defined.") + } +} + +// ScrapeMetric is an interface method to call scrapeGenericValues using Metric struct values +func (e *Exporter) ScrapeMetric(db *sql.DB, ch chan<- prometheus.Metric, metricDefinition Metric) error { + level.Debug(e.logger).Log("Calling function ScrapeGenericValues()") + return e.scrapeGenericValues(db, ch, metricDefinition.Context, metricDefinition.Labels, + metricDefinition.MetricsDesc, metricDefinition.MetricsType, metricDefinition.MetricsBuckets, + metricDefinition.FieldToAppend, metricDefinition.IgnoreZeroResult, + metricDefinition.Request) +} + +// generic method for retrieving metrics. +func (e *Exporter) scrapeGenericValues(db *sql.DB, ch chan<- prometheus.Metric, context string, labels []string, + metricsDesc map[string]string, metricsType map[string]string, metricsBuckets map[string]map[string]string, fieldToAppend string, ignoreZeroResult bool, request string) error { + metricsCount := 0 + genericParser := func(row map[string]string) error { + // Construct labels value + labelsValues := []string{} + for _, label := range labels { + labelsValues = append(labelsValues, row[label]) + } + // Construct Prometheus values to sent back + for metric, metricHelp := range metricsDesc { + value, err := strconv.ParseFloat(strings.TrimSpace(row[metric]), 64) + // If not a float, skip current metric + if err != nil { + level.Error(e.logger).Log("Unable to convert current value to float (metric=" + metric + + ",metricHelp=" + metricHelp + ",value=<" + row[metric] + ">)") + continue + } + level.Debug(e.logger).Log("Query result looks like: ", value) + // If metric do not use a field content in metric's name + if strings.Compare(fieldToAppend, "") == 0 { + desc := prometheus.NewDesc( + prometheus.BuildFQName(namespace, context, metric), + metricHelp, + labels, nil, + ) + if metricsType[strings.ToLower(metric)] == "histogram" { + count, err := strconv.ParseUint(strings.TrimSpace(row["count"]), 10, 64) + if err != nil { + level.Error(e.logger).Log("Unable to convert count value to int (metric=" + metric + + ",metricHelp=" + metricHelp + ",value=<" + row["count"] + ">)") + continue + } + buckets := make(map[float64]uint64) + for field, le := range metricsBuckets[metric] { + lelimit, err := strconv.ParseFloat(strings.TrimSpace(le), 64) + if err != nil { + level.Error(e.logger).Log("Unable to convert bucket limit value to float (metric=" + metric + + ",metricHelp=" + metricHelp + ",bucketlimit=<" + le + ">)") + continue + } + counter, err := strconv.ParseUint(strings.TrimSpace(row[field]), 10, 64) + if err != nil { + level.Error(e.logger).Log("Unable to convert ", field, " value to int (metric="+metric+ + ",metricHelp="+metricHelp+",value=<"+row[field]+">)") + continue + } + buckets[lelimit] = counter + } + ch <- prometheus.MustNewConstHistogram(desc, count, value, buckets, labelsValues...) + } else { + ch <- prometheus.MustNewConstMetric(desc, getMetricType(metric, metricsType), value, labelsValues...) + } + // If no labels, use metric name + } else { + desc := prometheus.NewDesc( + prometheus.BuildFQName(namespace, context, cleanName(row[fieldToAppend])), + metricHelp, + nil, nil, + ) + if metricsType[strings.ToLower(metric)] == "histogram" { + count, err := strconv.ParseUint(strings.TrimSpace(row["count"]), 10, 64) + if err != nil { + level.Error(e.logger).Log("Unable to convert count value to int (metric=" + metric + + ",metricHelp=" + metricHelp + ",value=<" + row["count"] + ">)") + continue + } + buckets := make(map[float64]uint64) + for field, le := range metricsBuckets[metric] { + lelimit, err := strconv.ParseFloat(strings.TrimSpace(le), 64) + if err != nil { + level.Error(e.logger).Log("Unable to convert bucket limit value to float (metric=" + metric + + ",metricHelp=" + metricHelp + ",bucketlimit=<" + le + ">)") + continue + } + counter, err := strconv.ParseUint(strings.TrimSpace(row[field]), 10, 64) + if err != nil { + level.Error(e.logger).Log("Unable to convert ", field, " value to int (metric="+metric+ + ",metricHelp="+metricHelp+",value=<"+row[field]+">)") + continue + } + buckets[lelimit] = counter + } + ch <- prometheus.MustNewConstHistogram(desc, count, value, buckets) + } else { + ch <- prometheus.MustNewConstMetric(desc, getMetricType(metric, metricsType), value) + } + } + metricsCount++ + } + return nil + } + level.Debug(e.logger).Log("Calling function GeneratePrometheusMetrics()") + err := e.generatePrometheusMetrics(db, genericParser, request) + level.Debug(e.logger).Log("ScrapeGenericValues() - metricsCount: ", metricsCount) + if err != nil { + return err + } + if !ignoreZeroResult && metricsCount == 0 { + return errors.New("No metrics found while parsing") + } + return err +} + +// inspired by https://kylewbanks.com/blog/query-result-to-map-in-golang +// Parse SQL result and call parsing function to each row +func (e *Exporter) generatePrometheusMetrics(db *sql.DB, parse func(row map[string]string) error, query string) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(e.config.QueryTimeout)*time.Second) + defer cancel() + rows, err := db.QueryContext(ctx, query) + + if ctx.Err() == context.DeadlineExceeded { + return errors.New("Oracle query timed out") + } + + if err != nil { + return err + } + cols, err := rows.Columns() + defer rows.Close() + + for rows.Next() { + // Create a slice of interface{}'s to represent each column, + // and a second slice to contain pointers to each item in the columns slice. + columns := make([]interface{}, len(cols)) + columnPointers := make([]interface{}, len(cols)) + for i := range columns { + columnPointers[i] = &columns[i] + } + + // Scan the result into the column pointers... + if err := rows.Scan(columnPointers...); err != nil { + return err + } + + // Create our map, and retrieve the value for each column from the pointers slice, + // storing it in the map with the name of the column as the key. + m := make(map[string]string) + for i, colName := range cols { + val := columnPointers[i].(*interface{}) + m[strings.ToLower(colName)] = fmt.Sprintf("%v", *val) + } + // Call function to parse row + if err := parse(m); err != nil { + return err + } + } + return nil +} + +func getMetricType(metricType string, metricsType map[string]string) prometheus.ValueType { + var strToPromType = map[string]prometheus.ValueType{ + "gauge": prometheus.GaugeValue, + "counter": prometheus.CounterValue, + "histogram": prometheus.UntypedValue, + } + + strType, ok := metricsType[strings.ToLower(metricType)] + if !ok { + return prometheus.GaugeValue + } + valueType, ok := strToPromType[strings.ToLower(strType)] + if !ok { + panic(errors.New("Error while getting prometheus type " + strings.ToLower(strType))) + } + return valueType +} + +func cleanName(s string) string { + s = strings.Replace(s, " ", "_", -1) // Remove spaces + s = strings.Replace(s, "(", "", -1) // Remove open parenthesis + s = strings.Replace(s, ")", "", -1) // Remove close parenthesis + s = strings.Replace(s, "/", "", -1) // Remove forward slashes + s = strings.Replace(s, "*", "", -1) // Remove asterisks + s = strings.ToLower(s) + return s +} + +func (e *Exporter) logError(s string) { + _ = level.Error(e.logger).Log(s) +} + +func (e *Exporter) logDebug(s string) { + _ = level.Debug(e.logger).Log(s) +} diff --git a/collector/default_metrics.go b/collector/default_metrics.go new file mode 100644 index 00000000..bfbe7e7b --- /dev/null +++ b/collector/default_metrics.go @@ -0,0 +1,82 @@ +package collector + +import ( + "errors" + + "github.com/BurntSushi/toml" + "github.com/go-kit/log/level" +) + +// needs the const if imported, cannot os.ReadFile in this case +const defaultMetricsConst = ` +[[metric]] +context = "sessions" +labels = [ "status", "type" ] +metricsdesc = { value= "Gauge metric with count of sessions by status and type." } +request = "SELECT status, type, COUNT(*) as value FROM v$session GROUP BY status, type" + +[[metric]] +context = "resource" +labels = [ "resource_name" ] +metricsdesc = { current_utilization= "Generic counter metric from v$resource_limit view in Oracle (current value).", limit_value="Generic counter metric from v$resource_limit view in Oracle (UNLIMITED: -1)." } +request="SELECT resource_name,current_utilization,CASE WHEN TRIM(limit_value) LIKE 'UNLIMITED' THEN '-1' ELSE TRIM(limit_value) END as limit_value FROM v$resource_limit" + +[[metric]] +context = "asm_diskgroup" +labels = [ "name" ] +metricsdesc = { total = "Total size of ASM disk group.", free = "Free space available on ASM disk group." } +request = "SELECT name,total_mb*1024*1024 as total,free_mb*1024*1024 as free FROM v$asm_diskgroup_stat where exists (select 1 from v$datafile where name like '+%')" +ignorezeroresult = true + +[[metric]] +context = "activity" +metricsdesc = { value="Generic counter metric from v$sysstat view in Oracle." } +fieldtoappend = "name" +request = "SELECT name, value FROM v$sysstat WHERE name IN ('parse count (total)', 'execute count', 'user commits', 'user rollbacks')" + +[[metric]] +context = "process" +metricsdesc = { count="Gauge metric with count of processes." } +request = "SELECT COUNT(*) as count FROM v$process" + +[[metric]] +context = "wait_time" +metricsdesc = { value="Generic counter metric from v$waitclassmetric view in Oracle." } +fieldtoappend= "wait_class" +request = ''' +SELECT + n.wait_class as WAIT_CLASS, + round(m.time_waited/m.INTSIZE_CSEC,3) as VALUE +FROM + v$waitclassmetric m, v$system_wait_class n +WHERE + m.wait_class_id=n.wait_class_id AND n.wait_class != 'Idle' +''' + +[[metric]] +context = "tablespace" +labels = [ "tablespace", "type" ] +metricsdesc = { bytes = "Generic counter metric of tablespaces bytes in Oracle.", max_bytes = "Generic counter metric of tablespaces max bytes in Oracle.", free = "Generic counter metric of tablespaces free bytes in Oracle.", used_percent = "Gauge metric showing as a percentage of how much of the tablespace has been used." } +request = ''' +SELECT + dt.tablespace_name as tablespace, + dt.contents as type, + dt.block_size * dtum.used_space as bytes, + dt.block_size * dtum.tablespace_size as max_bytes, + dt.block_size * (dtum.tablespace_size - dtum.used_space) as free, + dtum.used_percent +FROM dba_tablespace_usage_metrics dtum, dba_tablespaces dt +WHERE dtum.tablespace_name = dt.tablespace_name +ORDER by tablespace +''' +` + +// DefaultMetrics is a somewhat hacky way to load the default metrics +func (e *Exporter) DefaultMetrics() Metrics { + var metricsToScrape Metrics + if _, err := toml.Decode(defaultMetricsConst, &metricsToScrape); err != nil { + level.Error(e.logger).Log(err) + panic(errors.New("Error while loading " + defaultMetricsConst)) + } + return metricsToScrape +} diff --git a/go.mod b/go.mod index 5a30d6ad..00939f80 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.19 require ( github.com/BurntSushi/toml v1.2.1 github.com/alecthomas/kingpin/v2 v2.3.2 - github.com/go-kit/kit v0.12.0 github.com/go-kit/log v0.2.1 github.com/mattn/go-oci8 v0.1.1 github.com/prometheus/client_golang v1.14.0 @@ -26,7 +25,7 @@ require ( github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect - github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect golang.org/x/crypto v0.7.0 // indirect golang.org/x/net v0.8.0 // indirect diff --git a/go.sum b/go.sum index bbfbee29..c7edb653 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,6 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= -github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= @@ -51,8 +49,8 @@ github.com/prometheus/exporter-toolkit v0.9.1 h1:cNkC01riqiOS+kh3zdnNwRsbe/Blh0W github.com/prometheus/exporter-toolkit v0.9.1/go.mod h1:iFlTmFISCix0vyuyBmm0UqOUCTao9+RsAsKJP3YM9ec= github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= diff --git a/main.go b/main.go index 710ef602..cd27c61a 100644 --- a/main.go +++ b/main.go @@ -1,37 +1,26 @@ package main import ( - "bytes" "context" - "crypto/sha256" - "database/sql" - "errors" - "github.com/prometheus/exporter-toolkit/web" - webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag" - "hash" - "io" "net/http" "os" - "strconv" - "strings" - "sync" - "time" - "github.com/BurntSushi/toml" - "github.com/go-kit/kit/log" "github.com/go-kit/log/level" _ "github.com/mattn/go-oci8" - - "fmt" - "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/prometheus/common/version" + "github.com/prometheus/exporter-toolkit/web" + webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag" "github.com/alecthomas/kingpin/v2" "github.com/prometheus/common/promlog" "github.com/prometheus/common/promlog/flag" - //Required for debugging - //_ "net/http/pprof" + + // Required for debugging + // _ "net/http/pprof" + + "github.com/iamseth/oracledb_exporter/collector" ) var ( @@ -40,594 +29,66 @@ var ( metricPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics. (env: TELEMETRY_PATH)").Default(getEnv("TELEMETRY_PATH", "/metrics")).String() defaultFileMetrics = kingpin.Flag("default.metrics", "File with default metrics in a TOML file. (env: DEFAULT_METRICS)").Default(getEnv("DEFAULT_METRICS", "default-metrics.toml")).String() customMetrics = kingpin.Flag("custom.metrics", "File that may contain various custom metrics in a TOML file. (env: CUSTOM_METRICS)").Default(getEnv("CUSTOM_METRICS", "")).String() - queryTimeout = kingpin.Flag("query.timeout", "Query timeout (in seconds). (env: QUERY_TIMEOUT)").Default(getEnv("QUERY_TIMEOUT", "5")).String() + queryTimeout = kingpin.Flag("query.timeout", "Query timeout (in seconds). (env: QUERY_TIMEOUT)").Default(getEnv("QUERY_TIMEOUT", "5")).Int() maxIdleConns = kingpin.Flag("database.maxIdleConns", "Number of maximum idle connections in the connection pool. (env: DATABASE_MAXIDLECONNS)").Default(getEnv("DATABASE_MAXIDLECONNS", "0")).Int() maxOpenConns = kingpin.Flag("database.maxOpenConns", "Number of maximum open connections in the connection pool. (env: DATABASE_MAXOPENCONNS)").Default(getEnv("DATABASE_MAXOPENCONNS", "10")).Int() scrapeInterval = kingpin.Flag("scrape.interval", "Interval between each scrape. Default is to scrape on collect requests").Default("0s").Duration() toolkitFlags = webflag.AddFlags(kingpin.CommandLine, ":9161") ) -// Metric name parts. -const ( - namespace = "oracledb" - exporter = "exporter" -) - -// Metric object description -type Metric struct { - Context string - Labels []string - MetricsDesc map[string]string - MetricsType map[string]string - MetricsBuckets map[string]map[string]string - FieldToAppend string - Request string - IgnoreZeroResult bool -} - -// Metrics Used to load multiple metrics from file -type Metrics struct { - Metric []Metric -} - -// Metrics to scrap. Use external file (default-metrics.toml and custom if provided) -var ( - metricsToScrap Metrics - additionalMetrics Metrics - hashMap map[int][]byte -) - -// Exporter collects Oracle DB metrics. It implements prometheus.Collector. -type Exporter struct { - dsn string - duration, error prometheus.Gauge - totalScrapes prometheus.Counter - scrapeErrors *prometheus.CounterVec - scrapeResults []prometheus.Metric - up prometheus.Gauge - db *sql.DB - logger log.Logger -} - -// getEnv returns the value of an environment variable, or returns the provided fallback value -func getEnv(key, fallback string) string { - if value, ok := os.LookupEnv(key); ok { - return value - } - return fallback -} - -func maskDsn(dsn string) string { - parts := strings.Split(dsn, "@") - if len(parts) > 1 { - maskedUrl := "***@" + parts[1] - return maskedUrl - } - return dsn -} - -func connect(dsn string, logger log.Logger) *sql.DB { - level.Debug(logger).Log("msg", "Launching connection", "dsn", maskDsn(dsn)) - db, err := sql.Open("oci8", dsn) - if err != nil { - level.Error(logger).Log("msg", "Error while connecting to", "dsn", dsn) - panic(err) - } - level.Debug(logger).Log("msg", "set max idle connections to", "value", *maxIdleConns) - db.SetMaxIdleConns(*maxIdleConns) - level.Debug(logger).Log("msg", "set max open connections to", "value", *maxOpenConns) - db.SetMaxOpenConns(*maxOpenConns) - level.Debug(logger).Log("msg", "Successfully connected to", "dsn", maskDsn(dsn)) - return db -} - -// NewExporter returns a new Oracle DB exporter for the provided DSN. -func NewExporter(dsn string, logger log.Logger) *Exporter { - db := connect(dsn, logger) - return &Exporter{ - dsn: dsn, - duration: prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: namespace, - Subsystem: exporter, - Name: "last_scrape_duration_seconds", - Help: "Duration of the last scrape of metrics from Oracle DB.", - }), - totalScrapes: prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: exporter, - Name: "scrapes_total", - Help: "Total number of times Oracle DB was scraped for metrics.", - }), - scrapeErrors: prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: exporter, - Name: "scrape_errors_total", - Help: "Total number of times an error occurred scraping a Oracle database.", - }, []string{"collector"}), - error: prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: namespace, - Subsystem: exporter, - Name: "last_scrape_error", - Help: "Whether the last scrape of metrics from Oracle DB resulted in an error (1 for error, 0 for success).", - }), - up: prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: namespace, - Name: "up", - Help: "Whether the Oracle database server is up.", - }), - db: db, - logger: logger, - } -} - -// Describe describes all the metrics exported by the Oracle DB exporter. -func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { - // We cannot know in advance what metrics the exporter will generate - // So we use the poor man's describe method: Run a collect - // and send the descriptors of all the collected metrics. The problem - // here is that we need to connect to the Oracle DB. If it is currently - // unavailable, the descriptors will be incomplete. Since this is a - // stand-alone exporter and not used as a library within other code - // implementing additional metrics, the worst that can happen is that we - // don't detect inconsistent metrics created by this exporter - // itself. Also, a change in the monitored Oracle instance may change the - // exported metrics during the runtime of the exporter. - - metricCh := make(chan prometheus.Metric) - doneCh := make(chan struct{}) - - go func() { - for m := range metricCh { - ch <- m.Desc() - } - close(doneCh) - }() - - e.Collect(metricCh) - close(metricCh) - <-doneCh - -} - -// Collect implements prometheus.Collector. -func (e *Exporter) Collect(ch chan<- prometheus.Metric) { - if *scrapeInterval == 0 { // if we are to scrape when the request is made - e.scrape(ch) - } else { - scrapeResults := e.scrapeResults // There is a risk that e.scrapeResults will be replaced while we traverse this look. This should mitigate that risk - for idx := range scrapeResults { - ch <- scrapeResults[idx] - } - } - ch <- e.duration - ch <- e.totalScrapes - ch <- e.error - e.scrapeErrors.Collect(ch) - ch <- e.up -} - -func (e *Exporter) runScheduledScrapes() { - if *scrapeInterval == 0 { - return // Do nothing as scrapes will be done on Collect requests - } - ticker := time.NewTicker(*scrapeInterval) - defer ticker.Stop() - for { - metricCh := make(chan prometheus.Metric, 5) - go func() { - scrapeResults := []prometheus.Metric{} - for { - scrapeResult, more := <-metricCh - if more { - scrapeResults = append(scrapeResults, scrapeResult) - } else { - e.scrapeResults = scrapeResults - return - } - } - }() - e.scrape(metricCh) - close(metricCh) - <-ticker.C - } -} - -func (e *Exporter) scrape(ch chan<- prometheus.Metric) { - e.totalScrapes.Inc() - var err error - defer func(begun time.Time) { - e.duration.Set(time.Since(begun).Seconds()) - if err == nil { - e.error.Set(0) - } else { - e.error.Set(1) - } - }(time.Now()) - - if err = e.db.Ping(); err != nil { - if strings.Contains(err.Error(), "sql: database is closed") { - level.Info(e.logger).Log("msg", "Reconnecting to DB") - e.db = connect(e.dsn, e.logger) - } - } - if err = e.db.Ping(); err != nil { - level.Error(e.logger).Log("msg", "Error pinging oracle", "err", err) - //e.db.Close() - e.up.Set(0) - return - } else { - level.Debug(e.logger).Log("msg", "Successfully pinged Oracle database", "dsn", maskDsn(e.dsn)) - e.up.Set(1) - } - - if checkIfMetricsChanged(e.logger) { - reloadMetrics(e.logger) - } - - wg := sync.WaitGroup{} - - for _, metric := range metricsToScrap.Metric { - wg.Add(1) - metric := metric //https://golang.org/doc/faq#closures_and_goroutines - - go func() { - defer wg.Done() - - level.Debug(e.logger).Log("msg", "About to scrape metric") - level.Debug(e.logger).Log("metricsDesc", metric.MetricsDesc) - level.Debug(e.logger).Log("context", metric.Context) - level.Debug(e.logger).Log("metricsType", metric.MetricsType) - level.Debug(e.logger).Log("metricsBuckets", metric.MetricsBuckets) // , "(Ignored unless Histogram type)" - level.Debug(e.logger).Log("labels", metric.Labels) - level.Debug(e.logger).Log("fieldToAppend", metric.FieldToAppend) - level.Debug(e.logger).Log("ignoreZeroResult", metric.IgnoreZeroResult) - level.Debug(e.logger).Log("request", metric.Request) - - if len(metric.Request) == 0 { - level.Error(e.logger).Log("msg", "Error scraping. Did you forget to define request in your toml file?", "metricsDesc", metric.MetricsDesc) - return - } - - if len(metric.MetricsDesc) == 0 { - level.Error(e.logger).Log("msg", "Error scraping for query. Did you forget to define metricsdesc in your toml file?", "request", metric.Request) - return - } - - for column, metricType := range metric.MetricsType { - if metricType == "histogram" { - _, ok := metric.MetricsBuckets[column] - if !ok { - level.Error(e.logger).Log("msg", "Unable to find MetricsBuckets configuration key for metric", "metric", column) - return - } - } - } - - scrapeStart := time.Now() - if err = ScrapeMetric(e.db, ch, metric, e.logger); err != nil { - level.Error(e.logger).Log("msg", "Error scraping for", "context", metric.Context, "metricsDesc", metric.MetricsDesc, "since", time.Since(scrapeStart), "err", err) - e.scrapeErrors.WithLabelValues(metric.Context).Inc() - } else { - level.Debug(e.logger).Log("msg", "Successfully scraped metric", "context", metric.Context, "metricsDesc", metric.MetricsDesc, "since", time.Since(scrapeStart)) - } - }() - } - wg.Wait() -} - -func GetMetricType(metricType string, metricsType map[string]string) prometheus.ValueType { - var strToPromType = map[string]prometheus.ValueType{ - "gauge": prometheus.GaugeValue, - "counter": prometheus.CounterValue, - "histogram": prometheus.UntypedValue, - } - - strType, ok := metricsType[strings.ToLower(metricType)] - if !ok { - return prometheus.GaugeValue - } - valueType, ok := strToPromType[strings.ToLower(strType)] - if !ok { - panic(errors.New("Error while getting prometheus type " + strings.ToLower(strType))) - } - return valueType -} - -// ScrapeMetric interface method to call ScrapeGenericValues using Metric struct values -func ScrapeMetric(db *sql.DB, ch chan<- prometheus.Metric, metricDefinition Metric, logger log.Logger) error { - level.Debug(logger).Log("msg", "Calling function ScrapeGenericValues()") - return ScrapeGenericValues(db, ch, metricDefinition.Context, metricDefinition.Labels, - metricDefinition.MetricsDesc, metricDefinition.MetricsType, metricDefinition.MetricsBuckets, - metricDefinition.FieldToAppend, metricDefinition.IgnoreZeroResult, - metricDefinition.Request, logger) -} - -// ScrapeGenericValues generic method for retrieving metrics. -func ScrapeGenericValues(db *sql.DB, ch chan<- prometheus.Metric, context string, labels []string, - metricsDesc map[string]string, metricsType map[string]string, metricsBuckets map[string]map[string]string, fieldToAppend string, ignoreZeroResult bool, request string, logger log.Logger) error { - metricsCount := 0 - genericParser := func(row map[string]string) error { - // Construct labels value - var labelsValues []string - for _, label := range labels { - labelsValues = append(labelsValues, row[label]) - } - // Construct Prometheus values to sent back - for metric, metricHelp := range metricsDesc { - value, err := strconv.ParseFloat(strings.TrimSpace(row[metric]), 64) - // If not a float, skip current metric - if err != nil { - level.Error(logger).Log("msg", "Unable to convert current value to float", "metric", metric, - "metricHelp", metricHelp, "value", row[metric]) - continue - } - level.Debug(logger).Log("msg", "Query result looks like", "value", value) - // If metric do not use a field content in metric's name - if strings.Compare(fieldToAppend, "") == 0 { - desc := prometheus.NewDesc( - prometheus.BuildFQName(namespace, context, metric), - metricHelp, - labels, nil, - ) - if metricsType[strings.ToLower(metric)] == "histogram" { - count, err := strconv.ParseUint(strings.TrimSpace(row["count"]), 10, 64) - if err != nil { - level.Error(logger).Log("msg", "Unable to convert count value to int", "metric", metric, - "metricHelp", metricHelp, "value", row["count"]) - continue - } - buckets := make(map[float64]uint64) - for field, le := range metricsBuckets[metric] { - lelimit, err := strconv.ParseFloat(strings.TrimSpace(le), 64) - if err != nil { - level.Error(logger).Log("msg", "Unable to convert bucket limit value to float", "metric", metric, - "metricHelp", metricHelp, ",bucketlimit", le) - continue - } - counter, err := strconv.ParseUint(strings.TrimSpace(row[field]), 10, 64) - if err != nil { - level.Error(logger).Log("msg", "Unable to convert value to int", "field", field, "metric", metric, - "metricHelp", metricHelp, "value", row[field]) - continue - } - buckets[lelimit] = counter - } - ch <- prometheus.MustNewConstHistogram(desc, count, value, buckets, labelsValues...) - } else { - ch <- prometheus.MustNewConstMetric(desc, GetMetricType(metric, metricsType), value, labelsValues...) - } - // If no labels, use metric name - } else { - desc := prometheus.NewDesc( - prometheus.BuildFQName(namespace, context, cleanName(row[fieldToAppend])), - metricHelp, - nil, nil, - ) - if metricsType[strings.ToLower(metric)] == "histogram" { - count, err := strconv.ParseUint(strings.TrimSpace(row["count"]), 10, 64) - if err != nil { - level.Error(logger).Log("msg", "Unable to convert count value to int", "metric", metric, - "metricHelp", metricHelp, "value", row["count"]) - continue - } - buckets := make(map[float64]uint64) - for field, le := range metricsBuckets[metric] { - lelimit, err := strconv.ParseFloat(strings.TrimSpace(le), 64) - if err != nil { - level.Error(logger).Log("msg", "Unable to convert bucket limit value to float", "metric", metric, - "metricHelp", metricHelp, ",bucketlimit", le) - continue - } - counter, err := strconv.ParseUint(strings.TrimSpace(row[field]), 10, 64) - if err != nil { - level.Error(logger).Log("msg", "Unable to convert value to int", "field", field, "metric", metric, - "metricHelp", metricHelp, "value", row[field]) - continue - } - buckets[lelimit] = counter - } - ch <- prometheus.MustNewConstHistogram(desc, count, value, buckets) - } else { - ch <- prometheus.MustNewConstMetric(desc, GetMetricType(metric, metricsType), value) - } - } - metricsCount++ - } - return nil - } - level.Debug(logger).Log("msg", "Calling function GeneratePrometheusMetrics()") - err := GeneratePrometheusMetrics(db, genericParser, request, logger) - level.Debug(logger).Log("msg", "ScrapeGenericValues()", "metricsCount", metricsCount) - if err != nil { - return err - } - if !ignoreZeroResult && metricsCount == 0 { - return errors.New("No metrics found while parsing") - } - return err -} - -// GeneratePrometheusMetrics inspired by https://kylewbanks.com/blog/query-result-to-map-in-golang -// Parse SQL result and call parsing function to each row -func GeneratePrometheusMetrics(db *sql.DB, parse func(row map[string]string) error, query string, logger log.Logger) error { - - // Add a timeout - timeout, err := strconv.Atoi(*queryTimeout) - if err != nil { - level.Error(logger).Log("msg", "error while converting timeout option", "err", err) - panic(err) - } - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) - defer cancel() - rows, err := db.QueryContext(ctx, query) - - if ctx.Err() == context.DeadlineExceeded { - return errors.New("Oracle query timed out") - } - - if err != nil { - return err - } - cols, err := rows.Columns() - defer rows.Close() - - for rows.Next() { - // Create a slice of interface{}'s to represent each column, - // and a second slice to contain pointers to each item in the columns slice. - columns := make([]interface{}, len(cols)) - columnPointers := make([]interface{}, len(cols)) - for i := range columns { - columnPointers[i] = &columns[i] - } - - // Scan the result into the column pointers... - if err := rows.Scan(columnPointers...); err != nil { - return err - } +func main() { + promLogConfig := &promlog.Config{} + flag.AddFlags(kingpin.CommandLine, promLogConfig) + kingpin.HelpFlag.Short('\n') + kingpin.Version(version.Print("oracledb_exporter")) + kingpin.Parse() + logger := promlog.New(promLogConfig) + dsn := os.Getenv("DATA_SOURCE_NAME") - // Create our map, and retrieve the value for each column from the pointers slice, - // storing it in the map with the name of the column as the key. - m := make(map[string]string) - for i, colName := range cols { - val := columnPointers[i].(*interface{}) - m[strings.ToLower(colName)] = fmt.Sprintf("%v", *val) - } - // Call function to parse row - if err := parse(m); err != nil { - return err - } + config := &collector.Config{ + DSN: dsn, + MaxOpenConns: *maxOpenConns, + MaxIdleConns: *maxIdleConns, + CustomMetrics: *customMetrics, + QueryTimeout: *queryTimeout, } - - return nil - -} - -// Oracle gives us some ugly names back. This function cleans things up for Prometheus. -func cleanName(s string) string { - s = strings.Replace(s, " ", "_", -1) // Remove spaces - s = strings.Replace(s, "(", "", -1) // Remove open parenthesis - s = strings.Replace(s, ")", "", -1) // Remove close parenthesis - s = strings.Replace(s, "/", "", -1) // Remove forward slashes - s = strings.Replace(s, "*", "", -1) // Remove asterisks - s = strings.Replace(s, "%", "", -1) // Remove percent - s = strings.Replace(s, "-", "", -1) // Remove hyphen - s = strings.ToLower(s) - return s -} - -func hashFile(h hash.Hash, fn string) error { - f, err := os.Open(fn) + exporter, err := collector.NewExporter(logger, config) if err != nil { - return err - } - defer f.Close() - if _, err := io.Copy(h, f); err != nil { - return err - } - return nil -} - -func checkIfMetricsChanged(logger log.Logger) bool { - for i, _customMetrics := range strings.Split(*customMetrics, ",") { - if len(_customMetrics) == 0 { - continue - } - level.Debug(logger).Log("msg", "Checking modifications in following metrics definition", "file", _customMetrics) - h := sha256.New() - if err := hashFile(h, _customMetrics); err != nil { - level.Error(logger).Log("msg", "Unable to get file hash", "err", err) - return false - } - // If any of files has been changed reload metrics - if !bytes.Equal(hashMap[i], h.Sum(nil)) { - level.Info(logger).Log("msg", "Metrics file has been changed. Reloading...", "file", _customMetrics) - hashMap[i] = h.Sum(nil) - return true - } - } - return false -} - -func reloadMetrics(logger log.Logger) { - // Truncate metricsToScrap - metricsToScrap.Metric = []Metric{} - - // Load default metrics - if _, err := toml.DecodeFile(*defaultFileMetrics, &metricsToScrap); err != nil { - level.Error(logger).Log("msg", err) - panic(errors.New("Error while loading " + *defaultFileMetrics)) - } else { - level.Info(logger).Log("msg", "Successfully loaded default metrics", "file", *defaultFileMetrics) + level.Error(logger).Log("unable to connect to DB", err) } - // If custom metrics, load it - if strings.Compare(*customMetrics, "") != 0 { - for _, _customMetrics := range strings.Split(*customMetrics, ",") { - if _, err := toml.DecodeFile(_customMetrics, &additionalMetrics); err != nil { - level.Error(logger).Log("msg", err) - panic(errors.New("Error while loading " + _customMetrics)) - } else { - level.Info(logger).Log("msg", "Successfully loaded custom metrics", "file", _customMetrics) - } - metricsToScrap.Metric = append(metricsToScrap.Metric, additionalMetrics.Metric...) - } - } else { - level.Info(logger).Log("msg", "No custom metrics defined") + if *scrapeInterval != 0 { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go exporter.RunScheduledScrapes(ctx, *scrapeInterval) } -} -func main() { - - var promlogConfig = &promlog.Config{} - flag.AddFlags(kingpin.CommandLine, promlogConfig) - - kingpin.Version("oracledb_exporter " + Version) - kingpin.HelpFlag.Short('h') - kingpin.Parse() - logger := promlog.New(promlogConfig) - - level.Info(logger).Log("msg", "Starting oracledb_exporter", "version", Version) - dsn := os.Getenv("DATA_SOURCE_NAME") - - // Load default and custom metrics - hashMap = make(map[int][]byte) - reloadMetrics(logger) - - exporter := NewExporter(dsn, logger) prometheus.MustRegister(exporter) - go exporter.runScheduledScrapes() + prometheus.MustRegister(version.NewCollector("oracledb_exporter")) - // See more info on https://github.com/prometheus/client_golang/blob/master/prometheus/promhttp/http.go#L269 - opts := promhttp.HandlerOpts{ + level.Info(logger).Log("msg", "Starting oracledb_exporter", "version", version.Info()) + level.Info(logger).Log("msg", "Build context", "build", version.BuildContext()) + level.Info(logger).Log("msg", "Collect from: ", "metricPath", *metricPath) + opts := promhttp.HandlerOpts{ ErrorHandling: promhttp.ContinueOnError, } http.Handle(*metricPath, promhttp.HandlerFor(prometheus.DefaultGatherer, opts)) - http.HandleFunc("/scrape", scrapeHandle(logger)) - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("