Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Golang metrics prototype #100

Merged
merged 36 commits into from
Oct 8, 2019

Conversation

jmacd
Copy link
Contributor

@jmacd jmacd commented Aug 16, 2019

This satisfies the changes described in RFC 0003, and 0009.
This does not incorporate metrics observers (RFC 0008).

This includes the proposed LabelSet changes from open-telemetry/oteps#49

@@ -22,60 +22,156 @@ import (
"go.opentelemetry.io/api/unit"
)

type MetricType int
type Type int

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor suggestion from the peanut gallery: pick a word other that "type" for this. GCP uses "kind" so there's maybe some prior art for using that terminology. The word "type" can then be used exclusively to refer to int/float/etc (the value type) and "kind" can refer to the orthogonal idea of how to interpret those values.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, good suggestion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Member

@krnowak krnowak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments and ideas.

return ts.Variable.Defined()
}

func (ts TimeSeries) Description() TimeSeries {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point of this function? If it's to implement the Metric interface, then maybe add the var _ Metric = TimeSeries{} somewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was removed.

type Option func(*Handle, *[]registry.Option)
type Measurement struct {
Metric Metric
Value float64
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seems to be two types of fields, float and int. Should this struct also contain an int64 field then? The Metrics field would be the discriminator I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other languages, I've seen the SDK convert int->float in the exporter. If we must distinguish these types and not convert int->float->int, this will have to change.

@@ -69,6 +71,25 @@ func main() {

gauge.Set(ctx, 1)

meter.RecordBatch(ctx,
// TODO: Determine how the call-site should look for batch recording.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add some helpers in the metric package, like:

meter.RecordBatch(ctx,
	metric.Float64Measurement(oneMetric, 1.0, anotherKey.String("xyz")),
	metric.Int64Measurement(measureTwo, 42, anotherKey.String("zyx")),
)

(This assumes that the Measurement should have a separate field for int64 metrics.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In OpenCensus there was a syntax like

  meter.RecordBatch(ctx, oneMetric.M(1.0), measureTwo.M(42))

We've discussed dropping call-site labels from the spec, so anotherKey.String("xyz") disappears from the example.

case observer.BATCH_METRIC:
buf.WriteString("BATCH ")
for _, m := range data.Measurements {
// TODO: This breaks down because Gauge supports more than
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like Measurement is ambiguous in this case. Just an idea: should metric.Measurement should be an interface and gets returned by the implementations of the Float64Gauge and others interfaces? So the Float64Gauge interface would have two more functions like:

SetMeasurement(ctx context.Context, value float64, labels ...core.KeyValue) Measurement
AddMeasurement(ctx context.Context, value float64, labels ...core.KeyValue) Measurement

or we can keep Measurement as a concrete type (we seem to have a problem with interfacing all the things) and then add another enum:

type MeasurementKind int

const (
	F64GaugeAdd MeasurementKind = iota
	F64GaugeSet
	F64CumulativeInc
	F64MeasureRecord// same for I64
)

and Measurement would have an additional Kind field. Then we wouldn't need to add anything to the Float64Gauge interface and others.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With that change, maybe we won't need both SET_METRIC and UPDATE_METRIC. That information would already be a part of the Measurement object.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (and agreed in our working session).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot to add that there was some discussion about this, and a decision to support only MeasureMetric updates in the RecordBatch API. As it stands in the PR, any metric could be passed, which I actually prefer, but it's something we could change later.

buf.WriteString(" {")
i := 0
if m.Tags != nil {
// Note: This if-conditional can be removed after Map is not
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just need another approver to review #89. This goes a bit slowly…

@cep21
Copy link

cep21 commented Aug 27, 2019

Hi,

I understand this is marked as "do not review", but I would caution against the implemented Meter interface. It has a large number of methods. Go works best when interfaces have very few methods. Most interfaces in the go standard library contain a single method. Large interfaces are generally a code smell.

There exist alternative implementations that do not need such large interfaces. This talk at Gophercon 2016 talks about a similar thought process of how the random package in go can be broken down from a large interface into a smaller one.

@jmacd
Copy link
Contributor Author

jmacd commented Aug 27, 2019

Thanks, @cep21. I think I agree with your point in general, but the metrics interface is simply quite complicated. If you compare with the Prometheus Go interface,

https://godoc.org/github.com/prometheus/client_golang/prometheus

I think we've been able to reduce complexity without losing the optimization potential which is such a key requirement for OpenTelemetry--to match the Prometheus implementation for performance.

There are potential simplifications. Instead of 3 methods for float64 metrics and 3 methods for int64 metrics, we could have just 3 and use adapters to support int64 metrics. Or we could not support int64 metrics (I would agree to this, but the spec says to support both).

That said, the purpose of these interfaces is to have a separation between the API and the implementation. In this case, I opted for separate int64 and float64 metric interface methods because I believe there is someone out there who will argue that the API should not impose an int64->float64 conversion on the implementation. So, this interface is not trying to achieve the best Go interface in the traditional sense, it's trying to achieve the most complete separation of API and implementation possible.

Happy to hear if you have specific improvements in mind. I think the #metrics-specs channel in Gitter would be a good place to discuss this. Thanks.

@cep21
Copy link

cep21 commented Aug 27, 2019

but the metrics interface is simply quite complicated

I think the rand example I linked above is a good way to resolve this. I'm comparing it to interfaces I've written in the past. The closest thing available right now that is similar is the interface of https://github.com/segmentio/stats/blob/master/handler.go#L15. I believe with the right concrete abstractions on top of this interface opentelementry-go can achieve everything it needs and have an API that is a single method.

reduce complexity without losing the optimization potential

It will also be more efficient than the currently proposed API in terms of fewer global locks and less memory allocation per method, if a few tricks are used.

I think the #metrics-specs channel in Gitter would be a good place to discuss this

I'm worried a chat room is the wrong place for long form proposals. Is there a documentation format that is preferred.

Copy link
Contributor Author

@jmacd jmacd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please take another look @krnowak @cep21, thanks.

type Option func(*Handle, *[]registry.Option)
type Measurement struct {
Metric Metric
Value float64
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other languages, I've seen the SDK convert int->float in the exporter. If we must distinguish these types and not convert int->float->int, this will have to change.

@jmacd
Copy link
Contributor Author

jmacd commented Aug 30, 2019

This is tracking open-telemetry/oteps#29

Copy link
Member

@krnowak krnowak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some nits and questions.

}

// Option supports specifying the various metric options.
type Option func(*Instrument, *[]registry.Option)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this prototype kind of icky, because it gives you an access to whole Instrument object, which allows your Option implementation to change anything it wants there, only to find out that the Kind and Variable fields of the Instrument are clobbered by the registerInstrument function…

I wonder if it wouldn't be better to have the usual Options struct:

type Options struct {
	Desc string // will be translated to registry.WithDescription(desc)
	Unit unit.Unit // will be translated to registry.WithUnit(unit)
	Keys []core.Keys // will become Keys field of the instrument
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm all in favor of using an options struct here. I will update later in this PR, since the spec says to add options for positive-only, etc.

oneMetric,
lemonsKey.Int(10),
)
gauge := oneMetric.Handle(ctx, meter, lemonsKey.Int(10))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this also use fooKey and barKey? Or maybe it would be good to describe a comment what happens here if those keys are missing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I need to update the RFC about this topic, it was discussed heavily in the working session.

)
gauge := oneMetric.Handle(ctx, meter, lemonsKey.Int(10))

measure := measureTwo.Handle(ctx, meter, lemonsKey.Int(10))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar situation here - the lemonsKey is going to be ignored, because is not a part of predefined keys, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll update the RFC, this requires detail.

tag.NewContext(ctx,
tag.Insert(anotherKey.String("xyz"))),

metric.Measurement{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, is there no possibility to define values for the predefined keys when doing batched measurements?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handles contain the pre-defined label values. There's no ability to set additional labels on the call site, as agreed to in the working session.

Parent ScopeID // START_SPAN
Stats []stats.Measurement
Stat stats.Measurement
String string // START_SPAN, EVENT, ...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like SET_NAME disappeared here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

buf.WriteString(fmt.Sprint(m.Value))
buf.WriteString(" {")
i := 0
if m.Tags.Len() != 0 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m.Tags.Foreach should work perfectly fine on an empty map, so this if condition can be now removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Yeah, it was a nil interface before)

read.SpanContext = span.spanContext
if event.Context != nil {
span := trace.CurrentSpan(event.Context)
if span != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we warn about /ignore the event if the span is nil? The spanlog reporter is going to panic in this situation…

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should treat the metric event as though it has no span context, otherwise it should be OK


if event.Context != nil {
span := trace.CurrentSpan(event.Context)
if span != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, just a nil check looks good. If the reader will panic because SpanContext isn't set, let's fix that.

for _, r := range s.readers {
r.Read(span)
}
delete(s.spans, data.SpanContext)
}
}

func (s *spanReader) updateMetric(data reader.Event) {
// TODO aggregate
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to have this addressed before the PR is merged. Maybe this will uncover some problems with the API?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. I was planning to attach some other metrics libraries instead.
We're also looking to port over the OpenCensus metrics exporter, which will satisfy this TODO.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have the start of an SDK based on several-changes-ago of this branch, which I'll update today, and then we'll have a better look at the big picture.

scope observer.ScopeID
}

func (h *metricHandle) RecordAt(ctx context.Context, t time.Time, value float64) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

t time.Time parameter seems to be ignored. Should we set the Time field of the Event with it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, what's the meaning of the time.Time parameter? I see it can be set for each record when recording metrics one by one, but it can't be set for the batch updates at all. Should batch updates have a timestamp for each measurement or one for all of them or none at all (as it is currently)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm removing it. It's something we could specify later. I'm not sure how important users will find the ability to provide custom timestamps on metrics. We have custom timestamps on spans, but it's less clear if this is needed for metrics.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how important users will find the ability to provide custom timestamps on metrics.

The primary motivation is that time is a critical part of time series data, so the core abstract should contain it even if the layers above do not. In the past I've used the zero value of time.Time to imply "real time". It could be possible people have their own buffering above open telemetry and want to use the timestamp of values when they enter that, not when they enter open telemetry.

type Handle struct {
// Instrument represents a named metric with recommended local-aggregation keys.
type Instrument struct {
// Variable defines the metric name, description, and unit.
Variable registry.Variable
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is description and kind part of the key?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could maybe use some work. There is a common Variable type which represents metrics and keys, since they both are named things which we're interested in describing.

There are some open questions about registration of key and metric names, to avoid passing around full structs. Is that what you're thinking? I believe there is a desire to allow metric instruments to be allocated as vars in the compilation unit where they are used. As it stands, metrics and keys are not bound to the SDK, so that they do not need a SDK to initialize. They can initialize before any SDK exists, which is part of the reason why the Registry isn't well defined yet.

For my interest in a streaming SDK, I would be happy to record an event with a unique ID assigned to each key or metric, along with metadata like the description, and then at runtime the unique ID would stand in for the string that was registered. So, I'd like all keys and metric names to be assigned unique IDs, but don't want a registry to try and maintain a map of all keys/metrics in the process.

// Recorder is the implementation-level interface to Set/Add/Record individual metrics.
type Recorder interface {
// RecordAt allows the SDK to observe a single metric event
RecordAt(ctx context.Context, t time.Time, value float64)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you saying people will have to do

c.Inc(ctx, 1)

rather than

c.Inc(1)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. The rationale is that this lets us associated the distributed context with the update, allowing us to record the update in a span or as a structured metric event with all of its labels, not only those defined in the handle.

Copy link

@cep21 cep21 Sep 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That totally makes sense to me, but it's very strange for a function to take a context and not use the context to time out or unblock itself.

It's like "Take this thing that implies timeouts and cancelation, but don't actually use it to timeout or cancel anything. Just use it as a grabbag"

I wonder if it is better to take one of these.

type Valuer interface {
Value(key interface{}) interface{}
}
var _ Valuer = context.Context(nil)

RecorderFor(context.Context, Instrument, ...core.KeyValue) Recorder

// RecordBatch atomically records a batch of measurements.
RecordBatch(context.Context, ...Measurement)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

atomically may be too strict a requirement. What if the buffer can only fit 2 of them? I think it's fine to accept those two and trigger back pressure logic for the rest.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When this was discussed, it was being done explicitly to support recording inter-dependent metrics, where you wouldn't want an update to be split apart because it could create inconsistency.

I'm not sure the caller ever wants us to block. I'm starting to think this API should return an error in this sort of condition,.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you return errors you're asking the caller to check them, which makes metric reporting verbose. It may be better to register an "onerror" callback with the system that takes measurements that fail, and the library user can decide what to do.

// Meter is an interface to the metrics portion of the OpenTelemetry SDK.
type Meter interface {
// RecorderFor returns a handle for observing single measurements.
RecorderFor(context.Context, Instrument, ...core.KeyValue) Recorder
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my APIs I invert this. ObserverAt(tsid, meta)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand. I think tsid corresponds with Instrument and meta corresponds with ...core.KeyValue. Do you prefer "At" to "For"? I stuck with "Record" as a stem because of the existing verbs in the spec, which talk about recording metrics. I could be convinced that "Observer" is as good.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the past I've broken time series data into 4 components

  1. Time
  2. Value
  3. Time series identifier (tsid)
  4. Metadata.

Time and value are obvious. The TSID is the unique identifier that metrics aggregate upon (for example, for SignalFx or Datadog or cloudwatch this is a metric name and dimensions/tags). Metadata are extra concepts that don't change aggregation. Those are description/unit/etc.

I usually clearly separate those 4 into their own components. I think opentelemetry tries to combine tsid and metadata into the same struct. It may not be possible to split (3) and (4) in a generic way.

// Handle contains a Recorder to support the implementation-defined
// behavior of reporting a single metric with pre-determined label
// values.
type Handle struct {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are the methods for handle defined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle doesn't have any methods. There are Gauge, Cumulative, and Measure handles that have the appropriate method name. The other way Handle is used is as an argument to RecordBatch.

// WithKeys applies the provided dimension keys.
// WithKeys applies recommended dimension keys. Multiple `WithKeys`
// options accumulate. The keys specified in this way are taken as
// the aggregation keys for Gauge and Cumulative.
func WithKeys(keys ...core.Key) Option {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are there keys without values?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are keys that specify which values must be specified in a handle for a metric. Instruments (Gauge, Cumulative, Measure) have a set of keys, Handles have a set of pre-defined key:values.

Some work on the RFC and terminology are needed here. These keys specify values that are always defined by a handle for this metric, values that will not be overridden by the context. Any subset of these keys may be used for efficient pre-aggregation in the SDK.

return "unknown"
}
// Defined returns true when the instrument has been registered.
func (inst Instrument) Defined() bool {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the Instrument need to exist?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what you're asking. There are two kinds of thing here, one metric instrument (which does not depend on the SDK, is a plain struct) and one metric handle (which does depend on the SDK, has pre-defined label values, contains an interface to record with).

Handle
}

type Int64CumulativeHandle struct {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference between a cumulative and handle?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, the Int64Cumulative contains the metric name and other details. Int64CumulativeHandle associates the metric name with a set of pre-defined label values. I'll add more documentation, sorry.

RecorderFor(context.Context, Instrument, ...core.KeyValue) Recorder

// RecordBatch atomically records a batch of measurements.
RecordBatch(context.Context, ...Measurement)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does RecordBatch need to exist if you can create it from RecorderFor

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I once proposed we remove this interface,
https://github.com/open-telemetry/oteps/blob/6fc43532a8acc9024c143fa85777d53d2e646208/0003-eliminate-stats-record.md

The argument in favor of keeping it was that about inter-dependent metrics. E.g., gRPC would like to record N statistics at the end of an RPC, but they want to be sure that either 0 or N are reflected in the outcome, otherwise metrics summarization will be inaccurate.

A secondary argument is that there may be less locking overhead.

metric.Meter
}

var _ SDK = (*sdk)(nil)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

People normally write this

var _ SDK = &sdk{}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like I've seen (*T)(nil) more often. It is the styled presented in Effective Go.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're totally right about that. Seeing * and nil together somewhat hurts my brain. It's unclear why that's a preferred style over &sdk{}.

@@ -44,7 +46,7 @@ var (
metric.WithDescription("A gauge set to 1.0"),
)

measureTwo = stats.NewMeasure("ex.com/two")
measureTwo = metric.NewFloat64Measure("ex.com/two")
)

func main() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand there is a main.go example, but it would help to use https://blog.golang.org/examples for each individual concept to make it clear which are the minimal parts for each thing it's trying to do. With the main.go approach it's unclear which aspects are needed for which example it's trying to give. Like:

  1. Minimal example of a counter
  2. Minimal example of having a dimension on a metric
  3. Minimal example of aggregating.

Maybe an example is

func ExampleGauge() {
oneMetric = metric.NewFloat64Gauge("ex.com/one",   metric.WithKeys(fooKey, barKey, lemonsKey))
gauge := oneMetric.Handle(ctx, meter, lemonsKey.Int(10))
gauge.Set(1)
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a // Output: to the end of your example and it becomes code that is also executed when you run go test

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another advantage of testable examples is that they usually integrate with an IDE and show up on help dialogs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good suggestion, I always like the examples.

api/metric/api.go Outdated Show resolved Hide resolved
experimental/streaming/exporter/reader/format/format.go Outdated Show resolved Hide resolved
@jmacd
Copy link
Contributor Author

jmacd commented Sep 13, 2019

This is now tracking the LabelSet proposal here:
open-telemetry/oteps#49

// Kind is the metric kind of this instrument.
Kind Kind

// Bidirectional implies this is an up-down Cumulative.
Copy link
Member

@paivagustavo paivagustavo Sep 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the use case for Bidirectional and Unidirectional? Do we need both variables or should this be changed to a single variable? if I get it correctly, maybe it could be renamed to Monotonic

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally (I think) there would be some kind of union discriminated by the metric kind. So if kind were Cumulative (or soon-to-be-Counter), then the only accessible member would be Bidirectional. If kind were Gauge then you could only access Unidirectional. Same goes for the Measure kind and the NonNegative field. But this is Go and you don't have it, so that's why this struct looks a bit like reflect.Type interface, where docs say that the function will panic if used with wrong Kind. Which maybe would be nice to follow here too - have Bidirectional, Unidirectional and NonNegative as function that check if the kind is Cumulative, Gauge or Measure, respectively and panic if it isn't?

Use cases? These are options that enable less common (I suppose) kinds of metrics - usually Cumulative only grows, but maybe there are some metrics where it is possible for it to shrink. Similar goes to Gauge being usually random and Measure being usually >= 0.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great point, and it belongs in the RFC 0003 discussion.. Please use this PR: open-telemetry/oteps#51

I like the idea of replacing "Unidirectional" with "Monotonic": this option applies to Gauges and is not the default behavior.

For Counters, Monotonic is the default behavior, so we still need a name for the option to support bi-directional counters. I'm not aware of a good opposite for monotonic. Do you have any suggestions?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sent the discussion to mentioned PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@krnowak I suppose the Option objects could be made specific to the particular constructor, so that NewCounter(..., NonMononic()) would build but NewCounter(..., NonNegative) would not.

Also FYI I updated the RFCs to use "Monotonic" but we're still working out whether "NonMonotonic" is a preferred to "BiDirectional".

Copy link
Member

@krnowak krnowak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more comments. What's missing still is docs. Especially those explaining the interplay between the , Handle, Instrument and LabelSet types.

Makefile Outdated
$(GOTEST) $(GOTEST_OPT) $(ALL_PKGS)

.PHONY: examples
examples:
@for ex in $(EXAMPLES); do (echo $${ex} && cd $$(dirname $${ex}) && go build $$(basename $${ex})); done
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would @for ex in $(examples); do go build $$(dirname $${ex}); done work too?

type Meter interface {
// TODO more Metric types
GetFloat64Gauge(ctx context.Context, gauge *Float64GaugeHandle, labels ...core.KeyValue) Float64Gauge
// DefineLabels returns a reference to a set of labels that
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is confusing me. The application sets the labels, so it already knows what are those. So I'm not sure what "set of labels that cannot be read by the application" is supposed to mean.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant that you cannot read the labels from a LabelSet. I don't think you would need to, but the point is that the SDK is not required to keep this information, it could potentially stream it and forget--retain only an identifier.

// Kind is the metric kind of this instrument.
Kind Kind

// Bidirectional implies this is an up-down Cumulative.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally (I think) there would be some kind of union discriminated by the metric kind. So if kind were Cumulative (or soon-to-be-Counter), then the only accessible member would be Bidirectional. If kind were Gauge then you could only access Unidirectional. Same goes for the Measure kind and the NonNegative field. But this is Go and you don't have it, so that's why this struct looks a bit like reflect.Type interface, where docs say that the function will panic if used with wrong Kind. Which maybe would be nice to follow here too - have Bidirectional, Unidirectional and NonNegative as function that check if the kind is Cumulative, Gauge or Measure, respectively and panic if it isn't?

Use cases? These are options that enable less common (I suppose) kinds of metrics - usually Cumulative only grows, but maybe there are some metrics where it is possible for it to shrink. Similar goes to Gauge being usually random and Measure being usually >= 0.

)

func registerMetric(name string, mtype MetricType, opts []Option, metric *Handle) {
var varOpts []registry.Option
func registerInstrument(name string, kind Kind, opts []Option, inst *Instrument) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the function check bogus set ups like NonNegative for GaugeKind and warn/panic in such cases? That way it would be easier to spot when we did something wrong.

type InstrumentID uint64

// Instrument represents a named metric with recommended local-aggregation keys.
type Instrument struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is more like what we called in the protocol and what OpenMetrics as well calls "MetricDescriptor"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm open to a change of Instrument to Descriptor.

krnowak added a commit to kinvolk/opentelemetry-go that referenced this pull request Sep 19, 2019
This is to shrink the PR open-telemetry#100.

The only place where the registry.Variable type was used was metrics,
so just inline that type into its only user. The use of the
registry.Variable type in core.Key was limited to the Name field.

The stats package also used the registry.Variable type, but seems that
also only the Name field was used and the package is going to be
dropped anyway.
rghetia pushed a commit that referenced this pull request Sep 19, 2019
This is to shrink the PR #100.

The only place where the registry.Variable type was used was metrics,
so just inline that type into its only user. The use of the
registry.Variable type in core.Key was limited to the Name field.

The stats package also used the registry.Variable type, but seems that
also only the Name field was used and the package is going to be
dropped anyway.
@jmacd
Copy link
Contributor Author

jmacd commented Sep 20, 2019

I believe @krnowak is going to copy this branch into a new one, so it's not forked from my repo.

@krnowak
Copy link
Member

krnowak commented Oct 7, 2019

Updated. Added some docs, but with every line of the docs I wrote I felt dumber and dumber (or just tired), so I'd be grateful for the review. Certainly more of them need to be written still.

Things to do or ideas:

  • Add some mock implementation of metrics and add more tests.
  • Moar docs. Likely about various option for Descriptor in doc.go.
  • Maybe make Observer type an interface to be implemented by SDK - that way I could hide the Descriptor stuff and in turn you couldn't do meter.NewHandle(observer.Descriptor, labels) because that would be a compiler error.
    • That would also make Descriptor of an observer inaccessible to the API users, and would be inconsistent with other metrics.
    • Other idea - have a separate ObserverDescriptor struct and some helper function to convert it to Descriptor for the SDK use.
  • Maybe implement some atomic ops in MeasurementValue? (Not sure.)
    • What would be the value of func (v *MeasurementValue) Add(raw uint64, kind ValueKind) for the SDK?

@jmacd
Copy link
Contributor Author

jmacd commented Oct 7, 2019

About the Observer problem, it looks like NewHandle is really causing the issue. Maybe we could change the signature of NewHandle to take an interface, one that is implemented by the other three kinds but not by Observer. It could be a no-op method called "SupportHandle()".
All four instruments would support a Descriptor() *Descriptor method, only the three that support handles would add the second method to satisfy the interface.

@jmacd
Copy link
Contributor Author

jmacd commented Oct 7, 2019

I don't feel we need to worry about atomic operations on MeasurementValues. I see them as a transfer type--the SDK will do what it needs.

@jmacd
Copy link
Contributor Author

jmacd commented Oct 7, 2019

I can't approve this because I opened it. But I approve.

func (g *Int64Gauge) Measurement(value int64) Measurement {
return g.int64Measurement(value)
}

// Set sets the value of the gauge to the passed value. The labels
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe rephrase it to "Set assigns the passed value to the value of the gauge".
Same for all 'Set' methods.

func (c *Float64Measure) Record(ctx context.Context, value float64, labels LabelSet) {
c.recordOne(ctx, NewFloat64MeasurementValue(value), labels)
}

// Record adds a new value to the list of measure's records. The
// labels should contain the keys and values specified in the measure
// with the WithKeys option.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe rephrase the above to "The labels should contain the keys and values for each key specified in the measure with the WithKeys option"
What is the expected behavior if it doesn't have all the keys-value pair in the label set?

Copy link
Contributor

@rghetia rghetia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of doc comments. Otherwise LGTM.

@rghetia rghetia merged commit be8fb0b into open-telemetry:master Oct 8, 2019
@jmacd jmacd deleted the jmacd/metrics_proposal2 branch October 29, 2019 21:09
@jmacd jmacd mentioned this pull request Nov 20, 2019
hstan referenced this pull request in hstan/opentelemetry-go Oct 15, 2020
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.29.1 to 1.30.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](grpc/grpc-go@v1.29.1...v1.30.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:metrics Part of OpenTelemetry Metrics pkg:API Related to an API package
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants