Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to retrieve apdex for multiple applications #15

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 100 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,105 @@
# Project specific
vendor
newrelic_exporter
dist
idea
.idea

### Go ###
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, build with `go test -c`
*.test

# Output of the go coverage tool
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Project specific
vendor
newrelic_exporter
dist
### Go Patch ###
/vendor/
/Godeps/

### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf

# Generated files
.idea/**/contentModel.xml

# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml

# Gradle
.idea/**/gradle.xml
.idea/**/libraries

# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules

# CMake
cmake-build-*/

# Mongo Explorer plugin
.idea/**/mongoSettings.xml

# File-based project format
*.iws

# IntelliJ
out/

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Cursive Clojure plugin
.idea/replstate.xml

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

# Editor-based Rest Client
.idea/httpRequests

# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser

### Intellij Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721

# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr

# Sonarlint plugin
.idea/sonarlint

# End of https://www.gitignore.io/api/go,intellij
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
FROM alpine:3.8 as alpine

RUN apk add -U --no-cache ca-certificates

FROM scratch
EXPOSE 9112
WORKDIR /
COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY newrelic_exporter .
ENTRYPOINT ["./newrelic_exporter"]
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Exports New Relic applications metrics data as prometheus metrics.

You must add New Relic applications that you want to export metrics in the `config.yml` file:
```yaml
loglevel: INFO # (optional) set log level default INFO
timespan: 1 # (optional) timespan the summary of apdexmetrics will be retrieved (last x minutes) default 1
applications:
- id: 31584797 #New Relic application ID
name: My Application #New Relic application name
Expand All @@ -20,7 +22,7 @@ applications:
Or with docker:

```console
docker run -p 9112:9112 -v /path/to/my/config.yml:/config.yml -e "NEWRELIC_API_KEY=${NEWRELIC_API_KEY}" caninjas/newrelic_exporter
docker run -p 9112:9112 -v /path/to/my/config.yml:/config.yml -e "NEWRELIC_API_KEY=${NEWRELIC_API_KEY}" willlie1/newrelic_exporter
```

### Flags
Expand Down
57 changes: 57 additions & 0 deletions collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ type newRelicCollector struct {
keyTransactionErrorRate *prometheus.Desc
keyTransactionResponseTime *prometheus.Desc
keyTransactionThroughput *prometheus.Desc
metricApdexScore *prometheus.Desc
metricApdexSatisfied *prometheus.Desc
metricApdexTolerating *prometheus.Desc
metricApdexFrustrating *prometheus.Desc
metricApdexCount *prometheus.Desc
metricApdexThreshold *prometheus.Desc
metricApdexThresholdMin *prometheus.Desc
}

// NewNewRelicCollector returns a prometheus collector which exports
Expand Down Expand Up @@ -66,6 +73,13 @@ func NewNewRelicCollector(apiURL, apiKey string, config config.Config) prometheu
keyTransactionErrorRate: newKeyTransactionDesc("error_rate"),
keyTransactionResponseTime: newKeyTransactionDesc("response_time"),
keyTransactionThroughput: newKeyTransactionDesc("throughput"),
metricApdexScore: newMeticDesc("apdex_score"),
metricApdexSatisfied: newMeticDesc("apdex_satisfied"),
metricApdexTolerating: newMeticDesc("apdex_tolerating"),
metricApdexFrustrating: newMeticDesc("apdex_frustrating"),
metricApdexCount: newMeticDesc("apdex_count"),
metricApdexThreshold: newMeticDesc("apdex_threshold"),
metricApdexThresholdMin: newMeticDesc("apdex_threshold_min"),
}
}

Expand Down Expand Up @@ -96,6 +110,15 @@ func newKeyTransactionDesc(name string) *prometheus.Desc {
)
}

func newMeticDesc(name string) *prometheus.Desc {
return prometheus.NewDesc(
prometheus.BuildFQName(namespace, "metric_", name),
"Application rolling three-to-four-minute average for "+strings.Replace(name, "_", " ", -1),
[]string{"app", "name"},
nil,
)
}

// Describe describes all the metrics exported by the NewRelic exporter.
// It implements prometheus.Collector.
func (c *newRelicCollector) Describe(ch chan<- *prometheus.Desc) {
Expand All @@ -114,6 +137,13 @@ func (c *newRelicCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.keyTransactionErrorRate
ch <- c.keyTransactionResponseTime
ch <- c.keyTransactionThroughput
ch <- c.metricApdexScore
ch <- c.metricApdexSatisfied
ch <- c.metricApdexTolerating
ch <- c.metricApdexFrustrating
ch <- c.metricApdexCount
ch <- c.metricApdexThreshold
ch <- c.metricApdexThresholdMin
}

// Collect fetches the metrics data from the NewRelic application and
Expand Down Expand Up @@ -148,6 +178,7 @@ func (c *newRelicCollector) Collect(ch chan<- prometheus.Metric) {
return
}
c.collectApplicationSummary(ch, app.Name, application)
c.collectMetricApdex(ch, app.Name, app.ID)

instances, err := c.client.ListInstances(app.ID)
if err != nil {
Expand Down Expand Up @@ -211,3 +242,29 @@ func (c *newRelicCollector) collectKeyTransactions(ch chan<- prometheus.Metric)
}
}
}

func (c *newRelicCollector) collectMetricApdex(ch chan<- prometheus.Metric,
appName string, applicationId int64) {
log.Infof("Collecting metrics from Apdex data for application '%s' with id '%d'", appName, applicationId)
apdexMetricNames, errNames := c.client.ListApdexMetricNames(applicationId)
if errNames != nil {
log.Errorf("Failed to get apdex metrics: %v", errNames)
return
}
apdexMetricData := c.client.ListApdexMetricData(applicationId, apdexMetricNames)
log.Infof("Retrieved %d metrics for application '%s' with id '%d'", len(apdexMetricData), appName, applicationId)
for _, apdexMetric := range apdexMetricData {
apdexValue := apdexMetric.ApdexValues[0].ApdexMetricValue // Since we summarize by one minute, will get only one apdexValue for each metric
if (newrelic.ApdexValue{}) != apdexValue {
ch <- prometheus.MustNewConstMetric(c.metricApdexScore, prometheus.GaugeValue, apdexValue.Score, appName, apdexMetric.Name)
ch <- prometheus.MustNewConstMetric(c.metricApdexSatisfied, prometheus.GaugeValue, float64(apdexValue.Satisfied), appName, apdexMetric.Name)
ch <- prometheus.MustNewConstMetric(c.metricApdexTolerating, prometheus.GaugeValue, float64(apdexValue.Tolerating), appName, apdexMetric.Name)
ch <- prometheus.MustNewConstMetric(c.metricApdexFrustrating, prometheus.GaugeValue, float64(apdexValue.Frustrating), appName, apdexMetric.Name)
ch <- prometheus.MustNewConstMetric(c.metricApdexCount, prometheus.GaugeValue, float64(apdexValue.Count), appName, apdexMetric.Name)
ch <- prometheus.MustNewConstMetric(c.metricApdexThreshold, prometheus.GaugeValue, apdexValue.Threshold, appName, apdexMetric.Name)
ch <- prometheus.MustNewConstMetric(c.metricApdexThresholdMin, prometheus.GaugeValue, apdexValue.ThresholdMin, appName, apdexMetric.Name)
} else if !strings.HasPrefix(apdexMetric.Name, "Supportability") { // Searching metrics on "Apdex" will return a supportability metric which we want to ignore
log.Warnf("Ignoring apdex metric '%s' because it's not reporting.", apdexMetric.Name)
}
}
}
2 changes: 2 additions & 0 deletions config.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
loglevel: INFO
timespan: 1
applications:
- id: 31584797
name: My Application
Expand Down
5 changes: 5 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
// Config represents the exporter configuration
type Config struct {
Applications []Application `yaml:"applications,omitempty"`
TimeSpan int `yaml:"timespan"`
LogLevel string `yaml:"loglevel"`
}

// Application represents a NewRelic application scrape configuration
Expand All @@ -29,6 +31,9 @@ func Parse(path string) Config {
if err := yaml.Unmarshal(bts, &config); err != nil {
log.With("path", path).Fatalf("Failed to unmarshall configuration file: %v", err)
}
if len(config.LogLevel) == 0 {
config.LogLevel = "INFO" // set default to "INFO"
}

return config
}
14 changes: 12 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package main

import (
"fmt"
"github.com/ContaAzul/newrelic_exporter/collector"
"net/http"

"github.com/ContaAzul/newrelic_exporter/collector"
"github.com/ContaAzul/newrelic_exporter/config"
"github.com/ContaAzul/newrelic_exporter/newrelic"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/log"
Expand All @@ -27,15 +28,24 @@ func main() {
kingpin.HelpFlag.Short('h')
kingpin.Parse()

var config = config.Parse(*configFile)
log.AddFlags(kingpin.CommandLine)
_, err := kingpin.CommandLine.Parse([]string{"--log.level", config.LogLevel})
if err != nil {
log.Fatal("Failed to set log level property", err)
}
log.Info("Starting newrelic_exporter ", version)

if *apiKey == "" {
log.Fatal("You must provide your New Relic API key")
}

var config = config.Parse(*configFile)
prometheus.MustRegister(collector.NewNewRelicCollector(defaultBaseURL, *apiKey, config))

if config.TimeSpan > 0 {
newrelic.TimeSpan = config.TimeSpan
}

http.Handle(*metricsPath, promhttp.Handler())
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, // nolint: gas, errcheck
Expand Down
3 changes: 3 additions & 0 deletions newrelic/Constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package newrelic

var TimeSpan = 1
Loading