-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Conversation
api/metric/api.go
Outdated
@@ -22,60 +22,156 @@ import ( | |||
"go.opentelemetry.io/api/unit" | |||
) | |||
|
|||
type MetricType int | |||
type Type int |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, good suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this 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.
api/metric/api.go
Outdated
return ts.Variable.Defined() | ||
} | ||
|
||
func (ts TimeSeries) Description() TimeSeries { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was removed.
api/metric/api.go
Outdated
type Option func(*Handle, *[]registry.Option) | ||
type Measurement struct { | ||
Metric Metric | ||
Value float64 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
example/basic/main.go
Outdated
@@ -69,6 +71,25 @@ func main() { | |||
|
|||
gauge.Set(ctx, 1) | |||
|
|||
meter.RecordBatch(ctx, | |||
// TODO: Determine how the call-site should look for batch recording. |
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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…
Hi, I understand this is marked as "do not review", but I would caution against the implemented 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. |
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. |
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.
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'm worried a chat room is the wrong place for long form proposals. Is there a documentation format that is preferred. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
api/metric/api.go
Outdated
type Option func(*Handle, *[]registry.Option) | ||
type Measurement struct { | ||
Metric Metric | ||
Value float64 |
There was a problem hiding this comment.
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.
This is tracking open-telemetry/oteps#29 |
There was a problem hiding this 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.
api/metric/api.go
Outdated
} | ||
|
||
// Option supports specifying the various metric options. | ||
type Option func(*Instrument, *[]registry.Option) |
There was a problem hiding this comment.
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
}
There was a problem hiding this comment.
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.
example/basic/main.go
Outdated
oneMetric, | ||
lemonsKey.Int(10), | ||
) | ||
gauge := oneMetric.Handle(ctx, meter, lemonsKey.Int(10)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
example/basic/main.go
Outdated
) | ||
gauge := oneMetric.Handle(ctx, meter, lemonsKey.Int(10)) | ||
|
||
measure := measureTwo.Handle(ctx, meter, lemonsKey.Int(10)) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
example/basic/main.go
Outdated
tag.NewContext(ctx, | ||
tag.Insert(anotherKey.String("xyz"))), | ||
|
||
metric.Measurement{ |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, ... |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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…
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, I think.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
experimental/streaming/sdk/metric.go
Outdated
scope observer.ScopeID | ||
} | ||
|
||
func (h *metricHandle) RecordAt(ctx context.Context, t time.Time, value float64) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
api/metric/api.go
Outdated
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
api/metric/api.go
Outdated
// 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) |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
api/metric/api.go
Outdated
RecorderFor(context.Context, Instrument, ...core.KeyValue) Recorder | ||
|
||
// RecordBatch atomically records a batch of measurements. | ||
RecordBatch(context.Context, ...Measurement) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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,.
There was a problem hiding this comment.
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.
api/metric/api.go
Outdated
// 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 |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
- Time
- Value
- Time series identifier (tsid)
- 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.
api/metric/api.go
Outdated
// Handle contains a Recorder to support the implementation-defined | ||
// behavior of reporting a single metric with pre-determined label | ||
// values. | ||
type Handle struct { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
api/metric/api.go
Outdated
// 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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
api/metric/api.go
Outdated
return "unknown" | ||
} | ||
// Defined returns true when the instrument has been registered. | ||
func (inst Instrument) Defined() bool { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
api/metric/cumulative.go
Outdated
Handle | ||
} | ||
|
||
type Int64CumulativeHandle struct { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
api/metric/api.go
Outdated
RecorderFor(context.Context, Instrument, ...core.KeyValue) Recorder | ||
|
||
// RecordBatch atomically records a batch of measurements. | ||
RecordBatch(context.Context, ...Measurement) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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{}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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:
- Minimal example of a counter
- Minimal example of having a dimension on a metric
- 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)
}
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
This is now tracking the |
api/metric/api.go
Outdated
// Kind is the metric kind of this instrument. | ||
Kind Kind | ||
|
||
// Bidirectional implies this is an up-down Cumulative. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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".
There was a problem hiding this 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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
api/metric/api.go
Outdated
// Kind is the metric kind of this instrument. | ||
Kind Kind | ||
|
||
// Bidirectional implies this is an up-down Cumulative. |
There was a problem hiding this comment.
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.
api/metric/common.go
Outdated
) | ||
|
||
func registerMetric(name string, mtype MetricType, opts []Option, metric *Handle) { | ||
var varOpts []registry.Option | ||
func registerInstrument(name string, kind Kind, opts []Option, inst *Instrument) { |
There was a problem hiding this comment.
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.
api/metric/api.go
Outdated
type InstrumentID uint64 | ||
|
||
// Instrument represents a named metric with recommended local-aggregation keys. | ||
type Instrument struct { |
There was a problem hiding this comment.
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"
There was a problem hiding this comment.
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
.
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.
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.
I believe @krnowak is going to copy this branch into a new one, so it's not forked from my repo. |
dbda224
to
9b9c274
Compare
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:
|
About the |
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. |
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 |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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?
There was a problem hiding this 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.
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] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tyler Yahn <[email protected]>
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