diff --git a/metrics/metricskey/constants.go b/metrics/metricskey/constants.go index 01e5adff7e..b2508807f8 100644 --- a/metrics/metricskey/constants.go +++ b/metrics/metricskey/constants.go @@ -29,6 +29,12 @@ const ( // LabelNamespaceName is the label for immutable name of the namespace that the service is deployed LabelNamespaceName = "namespace_name" + // LabelResponseCode is the label for the HTTP response status code. + LabelResponseCode = "response_code" + + // LabelResponseCodeClass is the label for the HTTP response status code class. For example, "2xx", "3xx", etc. + LabelResponseCodeClass = "response_code_class" + // ValueUnknown is the default value if the field is unknown, e.g. project will be unknown if Knative // is not running on GKE. ValueUnknown = "unknown" diff --git a/metrics/source_stats_reporter.go b/metrics/source_stats_reporter.go new file mode 100644 index 0000000000..6848230f84 --- /dev/null +++ b/metrics/source_stats_reporter.go @@ -0,0 +1,146 @@ +/* + * Copyright 2019 The Knative Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package metrics + +import ( + "context" + "strconv" + + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" + "knative.dev/pkg/metrics/metricskey" +) + +var ( + // eventCountM is a counter which records the number of events sent by the source. + eventCountM = stats.Int64( + "event_count", + "Number of events sent", + stats.UnitDimensionless, + ) +) + +type ReportArgs struct { + Namespace string + EventType string + EventSource string + Name string + ResourceGroup string +} + +// StatsReporter defines the interface for sending source metrics. +type StatsReporter interface { + // ReportEventCount captures the event count. It records one per call. + ReportEventCount(args *ReportArgs, responseCode int) error +} + +var _ StatsReporter = (*reporter)(nil) + +// reporter holds cached metric objects to report source metrics. +type reporter struct { + namespaceTagKey tag.Key + eventSourceTagKey tag.Key + eventTypeTagKey tag.Key + sourceNameTagKey tag.Key + sourceResourceGroupTagKey tag.Key + responseCodeKey tag.Key + responseCodeClassKey tag.Key +} + +// NewStatsReporter creates a reporter that collects and reports source metrics. +func NewStatsReporter() (StatsReporter, error) { + var r = &reporter{} + + // Create the tag keys that will be used to add tags to our measurements. + nsTag, err := tag.NewKey(metricskey.LabelNamespaceName) + if err != nil { + return nil, err + } + r.namespaceTagKey = nsTag + + eventSourceTag, err := tag.NewKey(metricskey.LabelEventSource) + if err != nil { + return nil, err + } + r.eventSourceTagKey = eventSourceTag + + eventTypeTag, err := tag.NewKey(metricskey.LabelEventType) + if err != nil { + return nil, err + } + r.eventTypeTagKey = eventTypeTag + + nameTag, err := tag.NewKey(metricskey.LabelImporterName) + if err != nil { + return nil, err + } + r.sourceNameTagKey = nameTag + + resourceGroupTag, err := tag.NewKey(metricskey.LabelImporterResourceGroup) + if err != nil { + return nil, err + } + r.sourceResourceGroupTagKey = resourceGroupTag + + responseCodeTag, err := tag.NewKey(metricskey.LabelResponseCode) + if err != nil { + return nil, err + } + r.responseCodeKey = responseCodeTag + responseCodeClassTag, err := tag.NewKey(metricskey.LabelResponseCodeClass) + if err != nil { + return nil, err + } + r.responseCodeClassKey = responseCodeClassTag + + // Create view to see our measurements. + err = view.Register( + &view.View{ + Description: eventCountM.Description(), + Measure: eventCountM, + Aggregation: view.Count(), + TagKeys: []tag.Key{r.namespaceTagKey, r.eventSourceTagKey, r.eventTypeTagKey, r.sourceNameTagKey, r.sourceResourceGroupTagKey, r.responseCodeKey, r.responseCodeClassKey}, + }, + ) + if err != nil { + return nil, err + } + + return r, nil +} + +func (r *reporter) ReportEventCount(args *ReportArgs, responseCode int) error { + ctx, err := r.generateTag(args, responseCode) + if err != nil { + return err + } + Record(ctx, eventCountM.M(1)) + return nil +} + +func (r *reporter) generateTag(args *ReportArgs, responseCode int) (context.Context, error) { + return tag.New( + context.Background(), + tag.Insert(r.namespaceTagKey, args.Namespace), + tag.Insert(r.eventSourceTagKey, args.EventSource), + tag.Insert(r.eventTypeTagKey, args.EventType), + tag.Insert(r.sourceNameTagKey, args.Name), + tag.Insert(r.sourceResourceGroupTagKey, args.ResourceGroup), + tag.Insert(r.responseCodeKey, strconv.Itoa(responseCode)), + tag.Insert(r.responseCodeClassKey, ResponseCodeClass(responseCode))) +} diff --git a/metrics/source_stats_reporter_test.go b/metrics/source_stats_reporter_test.go new file mode 100644 index 0000000000..a58acbad66 --- /dev/null +++ b/metrics/source_stats_reporter_test.go @@ -0,0 +1,79 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package metrics + +import ( + "net/http" + "testing" + + "knative.dev/pkg/metrics/metricskey" + "knative.dev/pkg/metrics/metricstest" +) + +// unregister, ehm, unregisters the metrics that were registered, by +// virtue of StatsReporter creation. +// Since golang executes test iterations within the same process, the stats reporter +// returns an error if the metric is already registered and the test panics. +func unregister() { + metricstest.Unregister("event_count") +} + +func TestStatsReporter(t *testing.T) { + t.Skip("Fails in PROW but not locally, needs further investigation") + + args := &ReportArgs{ + Namespace: "testns", + EventType: "dev.knative.event", + EventSource: "unit-test", + Name: "testimporter", + ResourceGroup: "testresourcegroup", + } + + r, err := NewStatsReporter() + if err != nil { + t.Fatalf("Failed to create a new reporter: %v", err) + } + // Without this `go test ... -count=X`, where X > 1, fails, since + // we get an error about view already being registered. + defer unregister() + + wantTags := map[string]string{ + metricskey.LabelNamespaceName: "testns", + metricskey.LabelEventType: "dev.knative.event", + metricskey.LabelEventSource: "unit-test", + metricskey.LabelImporterName: "testimporter", + metricskey.LabelImporterResourceGroup: "testresourcegroup", + metricskey.LabelResponseCode: "202", + metricskey.LabelResponseCodeClass: "2xx", + } + + // test ReportEventCount + expectSuccess(t, func() error { + return r.ReportEventCount(args, http.StatusAccepted) + }) + expectSuccess(t, func() error { + return r.ReportEventCount(args, http.StatusAccepted) + }) + metricstest.CheckCountData(t, "event_count", wantTags, 2) +} + +func expectSuccess(t *testing.T, f func() error) { + t.Helper() + if err := f(); err != nil { + t.Errorf("Reporter expected success but got error: %v", err) + } +} diff --git a/metrics/utils.go b/metrics/utils.go new file mode 100644 index 0000000000..7345cec920 --- /dev/null +++ b/metrics/utils.go @@ -0,0 +1,26 @@ +/* +Copyright 2019 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package metrics + +import "strconv" + +// ResponseCodeClass converts an HTTP response code to a string representing its response code class. +// E.g., The response code class is "5xx" for response code 503. +func ResponseCodeClass(responseCode int) string { + // Get the hundred digit of the response code and concatenate "xx". + return strconv.Itoa(responseCode/100) + "xx" +}