Skip to content
This repository was archived by the owner on Dec 3, 2024. It is now read-only.

Commit 6b3e2bb

Browse files
authored
Merge pull request #24 from odenio/main
Record counters as CUMULATIVE metrics
2 parents da14b6e + 5a05d30 commit 6b3e2bb

File tree

3 files changed

+142
-11
lines changed

3 files changed

+142
-11
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ In general the author of this package would recommend instrumenting custom metri
1212

1313
This package is intended as a way to publish metrics for applications that are _already_ instrumented with `go-metrics` without having to use a sidecar process like [stackdriver-prometheus-sidecar](https://github.com/Stackdriver/stackdriver-prometheus-sidecar).
1414

15+
## 🚨 Upgrading
16+
17+
Between v0.5.0 and v0.6.0, the behavior of the `IncrCounter()` method changed: previously it would create a `GAUGE` [metric kind](https://cloud.google.com/monitoring/api/v3/kinds-and-types), but from v0.6.0 forward it will create a `CUMULATIVE` metric kind. (See https://github.com/google/go-metrics-stackdriver/issues/18 for a discussion.)
18+
19+
However, once a [MetricDescriptor](https://cloud.google.com/logging/docs/reference/v2/rest/v2/projects.metrics#MetricDescriptor) has been created in Google Cloud Monitoring, its `metricKind` field cannot be changed. So if you have any _existing_ `GAUGE` metrics that were created by `IncrCounter()`, you will see errors in your logs when the v0.6.0 client attempts to update them and fails. Your options for handling this are:
20+
21+
1. Change the name of the metric you are passing to `IncrCounter` (creating a new metricDescriptor with a different name), or:
22+
2. Delete the existing metricDescriptor using the [delete API](https://cloud.google.com/monitoring/api/ref_v3/rest/v3/projects.metricDescriptors/delete) and let go-metrics re-create it as a `CUMULATIVE` metric
23+
24+
Additionally, v0.6.0 adds `ResetCounter()` and `ResetCounterWithLabels()` methods: calling these methods resets the counter value to zero.
25+
1526
## Details
1627

1728
[stackdriver.NewSink](https://godoc.org/github.com/google/go-metrics-stackdriver#NewSink)'s return value satisfies the go-metrics library's [MetricSink](https://godoc.org/github.com/armon/go-metrics#MetricSink) interface. When providing a `stackdriver.Sink` to libraries and applications instrumented against `MetricSink`, the metrics will be aggregated within this library and written to stackdriver as [Generic Task](https://cloud.google.com/monitoring/api/resources#tag_generic_task) timeseries metrics.

stackdriver.go

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,9 @@ func (s *Sink) deep() (time.Time, map[string]*gauge, map[string]*counter, map[st
325325
}
326326
for k, v := range s.counters {
327327
rCounters[k] = &counter{
328-
name: v.name,
329-
value: v.value,
328+
name: v.name,
329+
value: v.value,
330+
startTime: v.startTime,
330331
}
331332
}
332333
for k, v := range s.histograms {
@@ -365,11 +366,14 @@ func (s *Sink) report(ctx context.Context) {
365366
Type: fmt.Sprintf("custom.googleapis.com/%s%s", s.prefix, name),
366367
Labels: labels,
367368
},
368-
MetricKind: metricpb.MetricDescriptor_GAUGE,
369+
MetricKind: metricpb.MetricDescriptor_CUMULATIVE,
369370
Resource: resource,
370371
Points: []*monitoringpb.Point{
371372
{
372373
Interval: &monitoringpb.TimeInterval{
374+
StartTime: &googlepb.Timestamp{
375+
Seconds: v.startTime.Unix(),
376+
},
373377
EndTime: &googlepb.Timestamp{
374378
Seconds: end.Unix(),
375379
},
@@ -537,11 +541,51 @@ func (s *Sink) IncrCounterWithLabels(key []string, val float32, labels []metrics
537541

538542
c, ok := s.counters[n.hash]
539543
if ok {
544+
// counter exists; increment value
540545
c.value += float64(val)
546+
// start times cannot be over 25 hours old; reset after 24h
547+
age := time.Now().Unix() - c.startTime.Unix()
548+
if age > 86400 {
549+
// Start times _must_ be before the end time (which is set in Report()),
550+
// so backdate our new start time to 1ms before the observed time.
551+
c.startTime = time.Now().Add(time.Millisecond * -1)
552+
}
541553
} else {
554+
// init new counter
542555
s.counters[n.hash] = &counter{
543556
name: n,
544557
value: float64(val),
558+
// startTime must predate what GCM believes is "now" when we create the interval;
559+
// so backdate by 1ms
560+
startTime: time.Now().Add(time.Millisecond * -1),
561+
}
562+
}
563+
}
564+
565+
// ResetCounter resets a counter to zero
566+
func (s *Sink) ResetCounter(key []string) {
567+
s.ResetCounterWithLabels(key, nil)
568+
}
569+
570+
// ResetCounterWithLabels resets a counter to zero
571+
func (s *Sink) ResetCounterWithLabels(key []string, labels []metrics.Label) {
572+
n := newSeries(key, labels)
573+
s.mu.Lock()
574+
defer s.mu.Unlock()
575+
576+
initVal := float64(0)
577+
startTime := time.Now().Add(time.Millisecond * -1)
578+
c, ok := s.counters[n.hash]
579+
if ok {
580+
// counter exists, reset to 0 and reset startTime
581+
c.value = initVal
582+
c.startTime = startTime
583+
} else {
584+
// counter did not exist, init at 0 value
585+
s.counters[n.hash] = &counter{
586+
name: n,
587+
value: initVal,
588+
startTime: startTime,
545589
}
546590
}
547591
}
@@ -645,8 +689,9 @@ type gauge struct {
645689

646690
// https://cloud.google.com/monitoring/api/ref_v3/rest/v3/TimeSeries#point
647691
type counter struct {
648-
name *series
649-
value float64
692+
name *series
693+
value float64
694+
startTime time.Time
650695
}
651696

652697
// https://cloud.google.com/monitoring/api/ref_v3/rest/v3/TimeSeries#distribution

stackdriver_test.go

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// you may not use this file except in compliance with the License.
55
// You may obtain a copy of the License at
66
//
7-
// http://www.apache.org/licenses/LICENSE-2.0
7+
// http://www.apache.org/licenses/LICENSE-2.0
88
//
99
// Unless required by applicable law or agreed to in writing, software
1010
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -365,7 +365,7 @@ func TestSample(t *testing.T) {
365365
Metric: &metricpb.Metric{
366366
Type: "custom.googleapis.com/go-metrics/foo_bar_counter",
367367
},
368-
MetricKind: metricpb.MetricDescriptor_GAUGE,
368+
MetricKind: metricpb.MetricDescriptor_CUMULATIVE,
369369
Points: []*monitoringpb.Point{
370370
{
371371
Value: &monitoringpb.TypedValue{
@@ -402,7 +402,7 @@ func TestSample(t *testing.T) {
402402
"env": "dev",
403403
},
404404
},
405-
MetricKind: metricpb.MetricDescriptor_GAUGE,
405+
MetricKind: metricpb.MetricDescriptor_CUMULATIVE,
406406
Points: []*monitoringpb.Point{
407407
{
408408
Value: &monitoringpb.TypedValue{
@@ -438,7 +438,7 @@ func TestSample(t *testing.T) {
438438
Metric: &metricpb.Metric{
439439
Type: "custom.googleapis.com/go-metrics/foo_bar_counter",
440440
},
441-
MetricKind: metricpb.MetricDescriptor_GAUGE,
441+
MetricKind: metricpb.MetricDescriptor_CUMULATIVE,
442442
Points: []*monitoringpb.Point{
443443
{
444444
Value: &monitoringpb.TypedValue{
@@ -458,6 +458,81 @@ func TestSample(t *testing.T) {
458458
}
459459
},
460460
},
461+
{
462+
name: "reset counter",
463+
collect: func() {
464+
ss.IncrCounter([]string{"foo", "bar"}, 1.0)
465+
ss.IncrCounter([]string{"foo", "bar"}, 1.0)
466+
ss.ResetCounter([]string{"foo", "bar"})
467+
},
468+
createFn: func(t *testing.T) func(context.Context, *monitoringpb.CreateTimeSeriesRequest) (*emptypb.Empty, error) {
469+
return func(_ context.Context, req *monitoringpb.CreateTimeSeriesRequest) (*emptypb.Empty, error) {
470+
want := &monitoringpb.CreateTimeSeriesRequest{
471+
Name: "projects/foo",
472+
TimeSeries: []*monitoringpb.TimeSeries{
473+
{
474+
Metric: &metricpb.Metric{
475+
Type: "custom.googleapis.com/go-metrics/foo_bar_counter",
476+
},
477+
MetricKind: metricpb.MetricDescriptor_CUMULATIVE,
478+
Points: []*monitoringpb.Point{
479+
{
480+
Value: &monitoringpb.TypedValue{
481+
Value: &monitoringpb.TypedValue_DoubleValue{
482+
DoubleValue: 0.0,
483+
},
484+
},
485+
},
486+
},
487+
},
488+
},
489+
}
490+
if diff := diffCreateMsg(want, req); diff != "" {
491+
t.Errorf("unexpected CreateTimeSeriesRequest (-want +got):\n%s", diff)
492+
}
493+
return &emptypb.Empty{}, nil
494+
}
495+
},
496+
},
497+
{
498+
name: "reset counter with label",
499+
collect: func() {
500+
ss.IncrCounterWithLabels([]string{"foo", "bar"}, 1.0, []metrics.Label{{Name: "env", Value: "dev"}})
501+
ss.IncrCounterWithLabels([]string{"foo", "bar"}, 1.0, []metrics.Label{{Name: "env", Value: "dev"}})
502+
ss.ResetCounterWithLabels([]string{"foo", "bar"}, []metrics.Label{{Name: "env", Value: "dev"}})
503+
},
504+
createFn: func(t *testing.T) func(context.Context, *monitoringpb.CreateTimeSeriesRequest) (*emptypb.Empty, error) {
505+
return func(_ context.Context, req *monitoringpb.CreateTimeSeriesRequest) (*emptypb.Empty, error) {
506+
want := &monitoringpb.CreateTimeSeriesRequest{
507+
Name: "projects/foo",
508+
TimeSeries: []*monitoringpb.TimeSeries{
509+
{
510+
Metric: &metricpb.Metric{
511+
Type: "custom.googleapis.com/go-metrics/foo_bar_counter",
512+
Labels: map[string]string{
513+
"env": "dev",
514+
},
515+
},
516+
MetricKind: metricpb.MetricDescriptor_CUMULATIVE,
517+
Points: []*monitoringpb.Point{
518+
{
519+
Value: &monitoringpb.TypedValue{
520+
Value: &monitoringpb.TypedValue_DoubleValue{
521+
DoubleValue: 0.0,
522+
},
523+
},
524+
},
525+
},
526+
},
527+
},
528+
}
529+
if diff := diffCreateMsg(want, req); diff != "" {
530+
t.Errorf("unexpected CreateTimeSeriesRequest (-want +got):\n%s", diff)
531+
}
532+
return &emptypb.Empty{}, nil
533+
}
534+
},
535+
},
461536
{
462537
name: "gauge",
463538
collect: func() {
@@ -773,7 +848,7 @@ func TestExtract(t *testing.T) {
773848
"method": "bar",
774849
},
775850
},
776-
MetricKind: metricpb.MetricDescriptor_GAUGE,
851+
MetricKind: metricpb.MetricDescriptor_CUMULATIVE,
777852
Points: []*monitoringpb.Point{
778853
{
779854
Value: &monitoringpb.TypedValue{
@@ -914,7 +989,7 @@ func TestExtract(t *testing.T) {
914989
"method": "baz",
915990
},
916991
},
917-
MetricKind: metricpb.MetricDescriptor_GAUGE,
992+
MetricKind: metricpb.MetricDescriptor_CUMULATIVE,
918993
Points: []*monitoringpb.Point{
919994
{
920995
Value: &monitoringpb.TypedValue{

0 commit comments

Comments
 (0)