Skip to content

Instantly share code, notes, and snippets.

@DazWilkin
Created December 14, 2018 21:48
Show Gist options
  • Save DazWilkin/cb0592ff63fbb4fed1c473e7aeb5a3a5 to your computer and use it in GitHub Desktop.
Save DazWilkin/cb0592ff63fbb4fed1c473e7aeb5a3a5 to your computer and use it in GitHub Desktop.
[Trillian] OpenCensus monitoring
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
}
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