Skip to content
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
23 changes: 11 additions & 12 deletions collectors/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,22 @@ func (d *descriptorCache) Store(prefix string, data []*monitoring.MetricDescript
d.cache[prefix] = &entry
}

// collectorCache is a cache for MonitoringCollectors
type CollectorCache struct {
// collectorCache caches MonitoringCollectors keyed by (project, prefix-filter)
// for the duration of the configured TTL. It exists so HTTP scrape paths that
// rebuild per request can preserve delta-counter state across calls.
type collectorCache struct {
cache map[string]*collectorCacheEntry
lock sync.RWMutex
ttl time.Duration
}

// collectorCacheEntry is a cache entry for a MonitoringCollector
type collectorCacheEntry struct {
collector *MonitoringCollector
expiry time.Time
}

// NewCollectorCache returns a new CollectorCache with the given TTL
func NewCollectorCache(ttl time.Duration) *CollectorCache {
c := &CollectorCache{
func newCollectorCache(ttl time.Duration) *collectorCache {
c := &collectorCache{
cache: make(map[string]*collectorCacheEntry),
ttl: ttl,
}
Expand All @@ -97,9 +97,8 @@ func NewCollectorCache(ttl time.Duration) *CollectorCache {
return c
}

// Get returns a MonitoringCollector if the key is found and not expired
// If key is found it resets the TTL for the collector
func (c *CollectorCache) Get(key string) (*MonitoringCollector, bool) {
// Get returns the cached collector for key, refreshing its TTL on hit.
func (c *collectorCache) Get(key string) (*MonitoringCollector, bool) {
c.lock.RLock()
defer c.lock.RUnlock()

Expand All @@ -118,7 +117,7 @@ func (c *CollectorCache) Get(key string) (*MonitoringCollector, bool) {
return entry.collector, true
}

func (c *CollectorCache) Store(key string, collector *MonitoringCollector) {
func (c *collectorCache) Store(key string, collector *MonitoringCollector) {
entry := &collectorCacheEntry{
collector: collector,
expiry: time.Now().Add(c.ttl),
Expand All @@ -129,15 +128,15 @@ func (c *CollectorCache) Store(key string, collector *MonitoringCollector) {
c.cache[key] = entry
}

func (c *CollectorCache) cleanup() {
func (c *collectorCache) cleanup() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
c.removeExpired()
}
}

func (c *CollectorCache) removeExpired() {
func (c *collectorCache) removeExpired() {
c.lock.Lock()
defer c.lock.Unlock()

Expand Down
4 changes: 2 additions & 2 deletions collectors/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func TestCollectorCache(t *testing.T) {

t.Run("basic cache Op", func(t *testing.T) {
ttl := 1 * time.Second
cache := NewCollectorCache(ttl)
cache := newCollectorCache(ttl)
collector := createCollector("test-project")
key := "test-key"

Expand All @@ -102,7 +102,7 @@ func TestCollectorCache(t *testing.T) {

t.Run("multiple collectors", func(t *testing.T) {
ttl := 1 * time.Second
cache := NewCollectorCache(ttl)
cache := newCollectorCache(ttl)

collectors := map[string]*MonitoringCollector{
"test-key-1": createCollector("test-project-1"),
Expand Down
20 changes: 15 additions & 5 deletions collectors/monitoring_collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import (

"github.com/prometheus/client_golang/prometheus"
"google.golang.org/api/monitoring/v3"

"github.com/prometheus-community/stackdriver_exporter/utils"
)

const namespace = "stackdriver"
Expand All @@ -39,7 +37,7 @@ type MetricFilter struct {
func ParseMetricExtraFilters(raw []string) []MetricFilter {
out := make([]MetricFilter, 0, len(raw))
for _, entry := range raw {
prefix, filter := utils.SplitExtraFilter(entry, ":")
prefix, filter := splitExtraFilter(entry, ":")
if prefix == "" {
continue
}
Expand All @@ -51,6 +49,18 @@ func ParseMetricExtraFilters(raw []string) []MetricFilter {
return out
}

func splitExtraFilter(extraFilter string, separator string) (string, string) {
mPrefix := strings.SplitN(extraFilter, separator, 2)
if len(mPrefix) != 2 {
return "", ""
}
return mPrefix[0], mPrefix[1]
}

func projectResource(projectID string) string {
return "projects/" + projectID
}

type MonitoringCollector struct {
projectID string
metricsTypePrefixes []string
Expand Down Expand Up @@ -329,7 +339,7 @@ func (c *MonitoringCollector) reportMonitoringMetrics(ch chan<- prometheus.Metri

c.logger.Debug("retrieving Google Stackdriver Monitoring metrics with filter", "filter", filter)

timeSeriesListCall := c.monitoringService.Projects.TimeSeries.List(utils.ProjectResource(c.projectID)).
timeSeriesListCall := c.monitoringService.Projects.TimeSeries.List(projectResource(c.projectID)).
Filter(filter).
IntervalStartTime(startTime.Format(time.RFC3339Nano)).
IntervalEndTime(endTime.Format(time.RFC3339Nano))
Expand Down Expand Up @@ -396,7 +406,7 @@ func (c *MonitoringCollector) reportMonitoringMetrics(ch chan<- prometheus.Metri
}

c.logger.Debug("listing Google Stackdriver Monitoring metric descriptors starting with", "prefix", metricsTypePrefix)
if err := c.monitoringService.Projects.MetricDescriptors.List(utils.ProjectResource(c.projectID)).
if err := c.monitoringService.Projects.MetricDescriptors.List(projectResource(c.projectID)).
Filter(filter).
Pages(ctx, callback); err != nil {
errChannel <- err
Expand Down
49 changes: 49 additions & 0 deletions collectors/monitoring_collector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,52 @@ func TestParseMetricExtraFilters(t *testing.T) {
t.Fatalf("ParseMetricExtraFilters() = %#v, want %#v", got, want)
}
}

func TestSplitExtraFilter(t *testing.T) {
t.Parallel()

tests := []struct {
name string
input string
wantPrefix string
wantFilter string
}{
{
name: "incomplete filter returns empty",
input: "This_is__a-MetricName.Example/with/no/filter",
wantPrefix: "",
wantFilter: "",
},
{
name: "basic filter",
input: "This_is__a-MetricName.Example/with:filter.name=filter_value",
wantPrefix: "This_is__a-MetricName.Example/with",
wantFilter: "filter.name=filter_value",
},
{
name: "filter value containing the separator",
input: `This_is__a-MetricName.Example/with:filter.name="filter:value"`,
wantPrefix: "This_is__a-MetricName.Example/with",
wantFilter: `filter.name="filter:value"`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

gotPrefix, gotFilter := splitExtraFilter(tt.input, ":")
if gotPrefix != tt.wantPrefix || gotFilter != tt.wantFilter {
t.Fatalf("splitExtraFilter() = (%q, %q), want (%q, %q)", gotPrefix, gotFilter, tt.wantPrefix, tt.wantFilter)
}
})
}
}

func TestProjectResource(t *testing.T) {
t.Parallel()

if got := projectResource("fake-project-1"); got != "projects/fake-project-1" {
t.Fatalf("projectResource() = %q, want %q", got, "projects/fake-project-1")
}
}
23 changes: 19 additions & 4 deletions collectors/monitoring_metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,38 @@
package collectors

import (
"regexp"
"sort"
"strings"
"time"

"github.com/fatih/camelcase"
"github.com/prometheus/client_golang/prometheus"
"google.golang.org/api/monitoring/v3"

"sort"

"github.com/prometheus-community/stackdriver_exporter/hash"
"github.com/prometheus-community/stackdriver_exporter/utils"
)

var safeNameRE = regexp.MustCompile(`[^a-zA-Z0-9_]*$`)

func buildFQName(timeSeries *monitoring.TimeSeries) string {
// The metric name to report is composed by the 3 parts:
// 1. namespace is a constant prefix (stackdriver)
// 2. subsystem is the monitored resource type (ie gce_instance)
// 3. name is the metric type (ie compute.googleapis.com/instance/cpu/usage_time)
return prometheus.BuildFQName(namespace, utils.NormalizeMetricName(timeSeries.Resource.Type), utils.NormalizeMetricName(timeSeries.Metric.Type))
return prometheus.BuildFQName(namespace, normalizeMetricName(timeSeries.Resource.Type), normalizeMetricName(timeSeries.Metric.Type))
}

func normalizeMetricName(metricName string) string {
var parts []string
for _, word := range camelcase.Split(metricName) {
safe := strings.Trim(safeNameRE.ReplaceAllLiteralString(word, "_"), "_")
lower := strings.TrimSpace(strings.ToLower(safe))
if lower != "" {
parts = append(parts, lower)
}
}
return strings.Join(parts, "_")
Comment on lines +39 to +48
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you're just moving a function from one place to another, so completely out of scope for this PR, but... in prometheus/otlptranslator we have an API that does the same thing and doesn't depend on regex matching. It's probably a lot faster :)

}

type timeSeriesMetrics struct {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package utils_test
package collectors

import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
import "testing"

"testing"
)
func TestNormalizeMetricName(t *testing.T) {
t.Parallel()

func TestUtils(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Utils Suite")
got := normalizeMetricName("This_is__a-MetricName.Example/with:0totals")
want := "this_is_a_metric_name_example_with_0_totals"
if got != want {
t.Fatalf("normalizeMetricName() = %q, want %q", got, want)
}
}
Loading