Created
December 14, 2018 21:48
-
-
Save DazWilkin/cb0592ff63fbb4fed1c473e7aeb5a3a5 to your computer and use it in GitHub Desktop.
[Trillian] OpenCensus monitoring
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package opencensus | |
import ( | |
"context" | |
"fmt" | |
"log" | |
"math" | |
"strings" | |
"time" | |
"contrib.go.opencensus.io/exporter/stackdriver" | |
"github.com/golang/glog" | |
"github.com/google/trillian/monitoring" | |
"go.opencensus.io/exporter/prometheus" | |
"go.opencensus.io/stats" | |
"go.opencensus.io/stats/view" | |
"go.opencensus.io/tag" | |
) | |
const ( | |
separator = "_" | |
) | |
// MetricFactory allows the creation of OpenCensus measures and views | |
type MetricFactory struct { | |
Prefix string | |
} | |
func init() { | |
var err error | |
sd, err := stackdriver.NewExporter(stackdriver.Options{ | |
// MetricPrefix helps uniquely identify these metrics | |
//TODO(dazwilkin) How to create the exporter in order to use MetricFactory.Prefix | |
MetricPrefix: "trillian", | |
}) | |
if err != nil { | |
log.Fatal(err) | |
} | |
// Important to invoke Flush before exiting | |
defer sd.Flush() | |
// Prometheus Exporter provides an http.Handler for the metrics endpoint | |
//TODO(dazwilkin) How to provide this handler back to the Trillian service? | |
// e.g. mux.Handle("/metrics", pm) | |
pm, err := prometheus.NewExporter(prometheus.Options{ | |
Namespace: "trillian", | |
}) | |
if err != nil { | |
log.Fatal(err) | |
} | |
view.RegisterExporter(sd) | |
view.RegisterExporter(pm) | |
// Stackdriver requires 60s reporting period | |
view.SetReportingPeriod(60 * time.Second) | |
} | |
// checkLabelNames as required by OpenCensus fails if any label name | |
// -- contains non-printable ASCII | |
// -- or len is 0 or >256 | |
// Printable ASCII 32-126 inclusive | |
func checkLabelNames(labelNames []string) { | |
nonPrintableASCII := func(r rune) bool { return (r < 32 || r > 126) } | |
for _, labelName := range labelNames { | |
if len(labelName) == 0 || len(labelName) > 256 { | |
glog.Fatalf("OpenCensus label names must be between 1 and 256 characters") | |
} | |
if strings.IndexFunc(labelName, nonPrintableASCII) != -1 { | |
glog.Fatalf("OpenCensus label names must be printable ASCII; '%s' is not", labelName) | |
} | |
} | |
} | |
// createKeyMethods creates OpenCensus Tag Keys for each label name | |
func createTagKeys(labelNames []string) []tag.Key { | |
tagKeys := make([]tag.Key, len(labelNames)) | |
var err error | |
for i, labelName := range labelNames { | |
tagKeys[i], err = tag.NewKey(labelName) | |
if err != nil { | |
glog.Fatal(err) | |
} | |
} | |
return tagKeys | |
} | |
// createMeasureAndView creates the OpenCensus Measure used to record stats and a View for reporting them | |
// Measurements are made against the Measure (returned) | |
// These are reported against any Views created with the Measure but, once registered, a handle to the view is dropped | |
func createMeasureAndView(prefix, name, help string, aggregation *view.Aggregation, labelNames []string) *stats.Float64Measure { | |
if len(labelNames) >= 1 { | |
// OpenCensus requires labelNames be (printable) ASCII | |
checkLabelNames(labelNames) | |
} | |
//TODO(dazwilkin) Should the measure name be prefixed with the namespace? | |
prefixedName := prefix + separator + name | |
measure := stats.Float64(prefixedName, help, "1") | |
tagKeys := createTagKeys(labelNames) | |
var v *view.View | |
v = &view.View{ | |
Name: prefixedName, | |
Measure: measure, | |
Description: help, | |
Aggregation: aggregation, | |
TagKeys: tagKeys, | |
} | |
if err := view.Register(v); err != nil { | |
log.Fatal(err) | |
} | |
return measure | |
} | |
func forAllLabelsAValue(labels, values []string) error { | |
if len(labels) != len(values) { | |
return fmt.Errorf("Mismatched number of labels (%v) and values (%v)", len(labels), len(values)) | |
} | |
return nil | |
} | |
func assignValuesToLabels(ctx context.Context, labels, values []string) context.Context { | |
for i, value := range values { | |
// NewKey is idempotent and provides the Key so that we can insert its value | |
label := labels[i] | |
t, err := tag.NewKey(label) | |
if err != nil { | |
glog.Fatalf("Label [%s] not found", label) | |
} | |
// Insert the tag and its value | |
if glog.V(2) { | |
glog.Infof("[opencensus:assignValuesToLabels] %s:%s", label, value) | |
} | |
ctx, _ = tag.New(ctx, tag.Insert(t, value)) | |
} | |
return ctx | |
} | |
// NewCounter create a new Counter object backed by OpenCensus | |
func (ocmf MetricFactory) NewCounter(name, help string, labelNames ...string) monitoring.Counter { | |
//TODO(dazwilkin) What View Aggregation is best for "Counter"? (sum?) | |
measure := createMeasureAndView(ocmf.Prefix, name, help, view.Sum(), labelNames) | |
return &Counter{ | |
labelNames: labelNames, | |
measure: measure, | |
} | |
} | |
// NewGauge creates a new Gauge object backed by OpenCensus | |
func (ocmf MetricFactory) NewGauge(name, help string, labelNames ...string) monitoring.Gauge { | |
//TODO(dazwilkin) What View Aggregation is best for "Gauge"? (count+sum? lastvalue?) | |
measure := createMeasureAndView(ocmf.Prefix, name, help, view.Sum(), labelNames) | |
return &Gauge{ | |
labelNames: labelNames, | |
measure: measure, | |
} | |
} | |
// Ref: https://github.com/google/trillian/blob/master/monitoring/prometheus/metrics.go | |
// buckets returns a reasonable range of histogram upper limits for most | |
// latency-in-seconds usecases. | |
func buckets() []float64 { | |
// These parameters give an exponential range from 0.04 seconds to ~1 day. | |
num := 300 | |
b := 1.05 | |
scale := 0.04 | |
r := make([]float64, 0, num) | |
for i := 0; i < num; i++ { | |
r = append(r, math.Pow(b, float64(i))*scale) | |
} | |
return r | |
} | |
// NewHistogram creates a new Histogram object backed by Prometheus. | |
func (ocmf MetricFactory) NewHistogram(name, help string, labelNames ...string) monitoring.Histogram { | |
//TODO(dazwilkin) How is an OpenCensus Distribution treated by Stackdriver? | |
measure := createMeasureAndView(ocmf.Prefix, name, help, view.Distribution(buckets()...), labelNames) | |
return &Histogram{ | |
labelNames: labelNames, | |
measure: measure, | |
} | |
} | |
// Counter is a wrapper around OpenCensus object | |
type Counter struct { | |
labelNames []string | |
measure *stats.Float64Measure | |
} | |
// Inc adds 1 to a counter. | |
func (c *Counter) Inc(labelVals ...string) { | |
c.Add(1.0, labelVals...) | |
} | |
// Add adds the given amount to a counter. | |
func (c *Counter) Add(val float64, labelVals ...string) { | |
//TODO(dazwilkin) Are negative values permitted? Think not. | |
// Nothing to do | |
if val <= 0.0 { | |
return | |
} | |
if err := forAllLabelsAValue(c.labelNames, labelVals); err != nil { | |
glog.Error(err.Error()) | |
return | |
} | |
ctx := context.TODO() | |
ctx = assignValuesToLabels(ctx, c.labelNames, labelVals) | |
stats.Record(ctx, c.measure.M(val)) | |
} | |
// Value returns the amount of a counter. | |
func (c *Counter) Value(labelVals ...string) float64 { | |
if err := forAllLabelsAValue(c.labelNames, labelVals); err != nil { | |
glog.Error(err.Error()) | |
return 0.0 | |
} | |
glog.Error("Unable to return values for counters") | |
return 0.0 | |
} | |
// Gauge is a wrapper around an OpenCensus measurement and view | |
type Gauge struct { | |
labelNames []string | |
measure *stats.Float64Measure | |
} | |
// Inc adds 1 to the gauge | |
func (g *Gauge) Inc(labelVals ...string) { | |
g.Set(1.0, labelVals...) | |
} | |
// Dec subtracts 1 from the gauge | |
func (g *Gauge) Dec(labelVals ...string) { | |
glog.Error("Unable to decrement gauge values; need to know the current value but don't") | |
// g.Set(g.value-1.0, labelVals...) | |
} | |
// Add adds given value to the gauge | |
func (g *Gauge) Add(val float64, labelVals ...string) { | |
glog.Error("Unable to add to gauge values; need to know the current value but don't.") | |
// g.Set(val, labelVals...) | |
} | |
// Set sets the value of the gauge | |
func (g *Gauge) Set(val float64, labelVals ...string) { | |
if err := forAllLabelsAValue(g.labelNames, labelVals); err != nil { | |
glog.Error(err.Error()) | |
return | |
} | |
ctx := context.TODO() | |
ctx = assignValuesToLabels(ctx, g.labelNames, labelVals) | |
stats.Record(ctx, g.measure.M(val)) | |
} | |
// Value returns the value of the gauge | |
func (g *Gauge) Value(labelVals ...string) float64 { | |
if err := forAllLabelsAValue(g.labelNames, labelVals); err != nil { | |
glog.Error(err.Error()) | |
return 0.0 | |
} | |
glog.Error("Unable to return values for gauges") | |
return 0.0 | |
} | |
// Histogram is a wrapper around OpenCensus measurement | |
type Histogram struct { | |
labelNames []string | |
measure *stats.Float64Measure | |
} | |
// Observe records a measure | |
func (h *Histogram) Observe(val float64, labelVals ...string) { | |
if err := forAllLabelsAValue(h.labelNames, labelVals); err != nil { | |
glog.Error(err.Error()) | |
return | |
} | |
ctx := context.TODO() | |
ctx = assignValuesToLabels(ctx, h.labelNames, labelVals) | |
stats.Record(ctx, h.measure.M(val)) | |
} | |
// Info returns the count and sum of observations in the histogram | |
//TODO(dazwilkin) Unsure how to implement this for OpenCensus | |
func (h *Histogram) Info(labelVals ...string) (uint64, float64) { | |
if err := forAllLabelsAValue(h.labelNames, labelVals); err != nil { | |
glog.Error(err.Error()) | |
return 0, 0.0 | |
} | |
glog.Error("Unable to return values for histograms") | |
return 0, 0.0 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package opencensus | |
import ( | |
"testing" | |
"github.com/google/trillian/monitoring/testonly" | |
) | |
// TODO: [dazwilkin:181206] Test for non-ASCII tag keys|values | |
func TestCounter(t *testing.T) { | |
testonly.TestCounter(t, MetricFactory{Prefix: "TestCounter"}) | |
} | |
func TestGauge(t *testing.T) { | |
testonly.TestGauge(t, MetricFactory{Prefix: "TestGauge"}) | |
} | |
func TestHistogram(t *testing.T) { | |
testonly.TestHistogram(t, MetricFactory{Prefix: "TestHistogram"}) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment