diff --git a/stackdriver.go b/stackdriver.go index b13bfb2..f72c0cc 100644 --- a/stackdriver.go +++ b/stackdriver.go @@ -52,6 +52,7 @@ type Sink struct { bucketer BucketFn extractor ExtractLabelsFn + prefix string taskInfo *taskInfo mu sync.Mutex @@ -69,6 +70,9 @@ type Config struct { // variable parameters within a metric name. // Optional. Defaults to DefaultLabelExtractor. LabelExtractor ExtractLabelsFn + // Prefix of the metrics recorded. Defaults to "go-metrics/" so your metric "foo" will be recorded as + // "custom.googleapis.com/go-metrics/foo". + Prefix *string // The bucketer is used to determine histogram bucket boundaries // for the sampled metrics. This will execute before the LabelExtractor. // Optional. Defaults to DefaultBucketer. @@ -147,6 +151,7 @@ func NewSink(client *monitoring.MetricClient, config *Config) *Sink { s := &Sink{ client: client, extractor: config.LabelExtractor, + prefix: "go-metrics/", bucketer: config.Bucketer, interval: config.ReportingInterval, taskInfo: &taskInfo{ @@ -159,6 +164,13 @@ func NewSink(client *monitoring.MetricClient, config *Config) *Sink { debugLogs: config.DebugLogs, } + if config.Prefix != nil { + if isValidMetricsPrefix(*config.Prefix) { + s.prefix = *config.Prefix + } else { + log.Printf("%s is not valid string to be used as metrics name, using default value 'go-metrics/'", *config.Prefix) + } + } // apply defaults if not configured explicitly if s.extractor == nil { s.extractor = DefaultLabelExtractor @@ -207,6 +219,12 @@ func NewSink(client *monitoring.MetricClient, config *Config) *Sink { return s } +func isValidMetricsPrefix(s string) bool { + // start with alphanumeric, can contain underscore in path (expect first char), slash is used to separate path. + match, err := regexp.MatchString("^(?:[a-z0-9](?:[a-z0-9_]*)/?)*$", s) + return err == nil && match +} + func (s *Sink) flushMetrics(ctx context.Context) { if s.interval == 0*time.Second { return @@ -297,7 +315,7 @@ func (s *Sink) report(ctx context.Context) { } ts = append(ts, &monitoringpb.TimeSeries{ Metric: &metricpb.Metric{ - Type: path.Join("custom.googleapis.com", "go-metrics", name), + Type: fmt.Sprintf("custom.googleapis.com/%s%s", s.prefix, name), Labels: labels, }, MetricKind: metric.MetricDescriptor_GAUGE, @@ -330,7 +348,7 @@ func (s *Sink) report(ctx context.Context) { } ts = append(ts, &monitoringpb.TimeSeries{ Metric: &metricpb.Metric{ - Type: path.Join("custom.googleapis.com", "go-metrics", name), + Type: fmt.Sprintf("custom.googleapis.com/%s%s", s.prefix, name), Labels: labels, }, MetricKind: metric.MetricDescriptor_GAUGE, @@ -370,7 +388,7 @@ func (s *Sink) report(ctx context.Context) { ts = append(ts, &monitoringpb.TimeSeries{ Metric: &metricpb.Metric{ - Type: path.Join("custom.googleapis.com", "go-metrics", name), + Type: fmt.Sprintf("custom.googleapis.com/%s%s", s.prefix, name), Labels: labels, }, MetricKind: metric.MetricDescriptor_CUMULATIVE, diff --git a/stackdriver_test.go b/stackdriver_test.go index 73c1161..fd35af9 100644 --- a/stackdriver_test.go +++ b/stackdriver_test.go @@ -91,6 +91,93 @@ func BenchmarkReport10(b *testing.B) { benchmarkCopy(10, 10, 10, b) } func BenchmarkReport50(b *testing.B) { benchmarkCopy(50, 50, 50, b) } func BenchmarkReport100(b *testing.B) { benchmarkCopy(100, 100, 100, b) } +func sPtr(s string) *string { + return &s +} + +func TestNewSinkSetCustomPrefix(t *testing.T) { + tests := []struct { + name string + configPrefix *string + expectedPrefix string + }{ + { + name: "default to go-metrics/", + expectedPrefix: "go-metrics/", + }, + { + name: "set custom", + configPrefix: sPtr("cuSt0m_metrics"), + expectedPrefix: "cuSt0m_metrics", + }, + { + name: "default to go-metrics/ when given prefix is invalid", + configPrefix: sPtr("___"), + expectedPrefix: "go-metrics/", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ss := NewSink(nil, &Config{Prefix: tc.configPrefix}) + + if ss.prefix != tc.expectedPrefix { + t.Errorf("prefix should be initalized as '" + tc.expectedPrefix + "' but got " + ss.prefix) + } + }) + } +} + +func TestIsValidMetricsPrefix(t *testing.T) { + tests := []struct { + prefix string + expectedValid bool + }{ + { + prefix: "", + expectedValid: true, + }, + { + prefix: "a", + expectedValid: true, + }, + { + prefix: "abc/bef/", + expectedValid: true, + }, + { + prefix: "aa_", + expectedValid: true, + }, + { + prefix: "///", + expectedValid: false, + }, + { + prefix: "!", + expectedValid: false, + }, + { + prefix: "_aa", + expectedValid: false, + }, + { + prefix: "日本語", + expectedValid: false, + }, + } + for _, tc := range tests { + t.Run(tc.prefix, func(t *testing.T) { + if isValidMetricsPrefix(tc.prefix) != tc.expectedValid { + if tc.expectedValid { + t.Errorf("expected %s to be valid", tc.prefix) + } else { + t.Errorf("expected %s to be invalid", tc.prefix) + } + } + }) + } +} + func TestSample(t *testing.T) { ss := newTestSink(0*time.Second, nil) @@ -943,6 +1030,7 @@ func newTestSink(interval time.Duration, client *monitoring.MetricClient) *Sink s.taskInfo = &taskInfo{ ProjectID: "foo", } + s.prefix = "go-metrics/" s.interval = interval s.bucketer = DefaultBucketer s.extractor = DefaultLabelExtractor