-
Notifications
You must be signed in to change notification settings - Fork 4.4k
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
experimental/stats: Add metrics registry #7349
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #7349 +/- ##
==========================================
+ Coverage 80.58% 81.54% +0.95%
==========================================
Files 349 350 +1
Lines 34056 26843 -7213
==========================================
- Hits 27445 21889 -5556
+ Misses 5431 3770 -1661
- Partials 1180 1184 +4
|
0475578
to
7f9a5c3
Compare
7f9a5c3
to
a845ca6
Compare
Got to offline discussion items, and moved symbols around to where we discussed. |
stats/stats.go
Outdated
// Metric is an identifier for a metric. | ||
type Metric string |
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.
Shall we create a metrics.go
for these new things?
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.
Ah, good point. Done.
stats/stats.go
Outdated
// Metrics are the set of Metrics to initialize. | ||
Metrics map[Metric]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.
Shouldn't this be private? Maybe a ToSlice
method instead?
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 export this to read this field in OTel construction (determines whether a metric is created or not)...do you want me to expose a getter for a map and copy? That would prevent the user from doing operations on this?
|
||
import "google.golang.org/grpc/stats" | ||
|
||
// Int64CountHandle is a typed handle for a int count instrument. This handle is |
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.
*an int - throughout.
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.
// Int64CountHandle is a typed handle for a int count instrument. This handle is | ||
// passed at the recording point in order to know which instrument to record on. | ||
type Int64CountHandle struct { | ||
Index 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.
Does this need to be exported?
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, both the OpenTelemetry component and the MetricsRecorderList (the next two PRs) read this index out. (In MetricsRecorderList, it uses the index to verify the labels/optionalLabels (which will log at error and eat call, not panic like Java as discussed offline). OpenTelemetry uses this index as the invariant of instrument registry that it creates metrics from and needs the Index to know which created metric to record. (In the code I have, I create noops for unregistered metrics as well, and the index will match to that and be eaten).
// DefaultMetrics are the default metrics registered through global instruments | ||
// registry. This is written to at initialization time only, and is read | ||
// only after initialization. | ||
var DefaultMetrics = make(map[stats.Metric]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.
I don't think we want users to have direct access to this map? I think they just need a Register
type function exposed.
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 discussed this offline; I thought one of the requirements you gave me was to explicitly expose this, so users that write their own stats plugins/non per call interface can have access to 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.
They need to read the default metrics but not directly access a set of them.
Also: why not use the Metrics
set type for this?
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.
Oh ok good point. I did add Merge() so yeah this should be a MetricsSet, is having the exported type be a MetricsSet good enough for your point of reading but not accessing it: "They need to read the default metrics but not directly access a set of them."
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 merge will be called at runtime in OpenTelemetry for the default metrics as discussed offline.
* | ||
*/ | ||
|
||
// Package instrumentregistry is a instrument registry that gRPC components can |
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.
*an
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.
"google.golang.org/grpc/stats" | ||
) | ||
|
||
// InstrumentDescriptor is a data of a registered instrument (metric). |
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 data for"?
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.
Switched.
Name string | ||
// Description is the description of this metric. | ||
Description string | ||
// Unit is the unit of this metric. |
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.
Can we delete these comments? Or make them less tautological?
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 these are just for vet, is there a way to ignore vet for exported fields like this that are self explanatory?
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 can always improve them to make them more useful. Like give examples for units, explain what labels are used for, etc.
) | ||
|
||
// InstrumentDescriptor is a data of a registered instrument (metric). | ||
type InstrumentDescriptor 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.
Aren't these things that third-party plugins will eventually need to work with? (I.e. should be in experimental
?)
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.
Oh hmmmm you're right. So the only things kept internal are what a gRPC component calls into, and this will need to be read by external users in their stats plugins so they can create instruments based off 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.
I asked about this in the chat; thinking about it do I need to move the whole instrument registry into experimental actually? A user can also write their own balancer/resolver, which should be able to register instruments to eventually record on so I think that all the symbols/logic need to be external (in experimental). What do you 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.
Discussed in chat with Yash; decided to move this whole instrument registry to experimental and make externally visible.
// NOTE: this function must only be called during initialization time (i.e. in | ||
// an init() function), and is not thread-safe. If multiple instruments are | ||
// registered with the same name, this function will panic. | ||
func RegisterInt64Count(name string, desc string, unit string, labels []string, optionalLabels []string, def bool) instrumentregistry.Int64CountHandle { |
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.
How about having the user pass the InstrumentDescriptor
directly?
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.
Hmm ok. Thinking about it that would allow scaling up fields if needed in a backward compatible way if we decide we want to persist another piece of information here. Underlying though I'll persist around a copy of the struct not the same pointer as a user.
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 can pass by value instead of pointer to avoid the need to manually copy.
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 was thinking about it too now that I use a metrics set for defaults and there's a metrics type I might just have the inst desc take a Metric, and have the user pass in a Metric for explicit types and no need to typecast in this registry.
9ebf079
to
97b48c1
Compare
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 we check this in without any code using it, then we should probably be prepared to make a lot of changes here.
experimental/stats/metricregistry.go
Outdated
package stats | ||
|
||
import ( | ||
"log" |
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 don't use "log"
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.
Ah whoops I had this for panic formatting. Switched to logger.Fatalf().
experimental/stats/metricregistry.go
Outdated
Name stats.Metric | ||
// Description is the description of this metric. | ||
Description string | ||
// Unit is the unit of this metric (e.g. entries, milliseconds). |
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.
Isn't OTel opposed to using "milliseconds" and instead prefer to use "seconds" for most/all things time related?
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.
Oh yeah I thought the A66 metrics were /milliseconds, but they are defined as /s (https://github.com/grpc/proposal/blob/master/A66-otel-stats.md#client-per-attempt-instruments) which is why we had those bucket counts.
experimental/stats/metricregistry.go
Outdated
|
||
// MetricDescriptor is the data for a registered metric. | ||
type MetricDescriptor struct { | ||
// Name is the name of this metric. This name must be unique across whole |
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.
Docstrings on fields don't follow this convention like types/functions. You can just say "The name of the metric". Throughout. Maybe that's why it seems so redundant to me "Name is the name". Just say "The name".
https://google.github.io/styleguide/go/decisions#doc-comments
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 I'll just delete these comments.
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 switched comments for field to // The Field...
experimental/stats/metricregistry.go
Outdated
// is passed at the recording point in order to know which metric to record | ||
// on. | ||
type Int64CountHandle struct { | ||
MetricDescriptor *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.
Unexport this field throughout? Or maybe type Int64CountHandle MetricDescriptor
? Or type Int64CountHandle *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.
Hmm ok so we could do an alias too? I needed this exported for OpenTelemetry to read the pointer to look into it's data structures it builds out for metrics. (I guess the metrics recorder list could live here for the assertion on labels/optional labels length and access unexported fields).
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 I make it a pointer alias; I can't define functions on it. I tried making it a alias to metricDescriptor but the problem is if I typecast the descriptor into a xxxHandle it changes address. How do we keep it pointers so it can link alongside calling methods on handle returned?
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 you can point to a playground link illustrating the things you tried? The code as-is is still a struct so I can't see what you did.
experimental/stats/metricregistry.go
Outdated
} | ||
|
||
// Record records the int64 count value on the metrics recorder provided. | ||
func (h Int64CountHandle) Record(recorder MetricsRecorder, incr int64, labels ...string) { |
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 these just be passed around by pointer everywhere? Why 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.
Sure we could. This is a copy of a that x bit pointer but pointer works too.
experimental/stats/stats.go
Outdated
// Package stats contains experimental metrics/stats API's. | ||
package stats | ||
|
||
// MetricsRecorder records on metrics derived from instrument registry. |
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 don't seem to use "instrument" anymore right?
Also I think this should be in "metrics.go" or "metricregistry.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.
Ah whoops missed this file (I went on a crusade to switch all instances in other files). Switched.
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.
Let's leave this here for now until we wrap up discussion on whether metrics.go should be experimental or not.
experimental/stats/stats.go
Outdated
type MetricsRecorder interface { | ||
// RecordIntCount records the measurement alongside labels on the int count | ||
// associated with the provided handle. | ||
RecordIntCount(Int64CountHandle, int64, ...string) |
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's nice to name your parameters as a documentation aide.
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.
Ah, yeah I was wondering this too. Named the parameters handle, incr, and labels.
stats/metrics.go
Outdated
import "maps" | ||
|
||
// Metric is an identifier for a metric. | ||
type Metric string |
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.
Don't we want all these to be in experimental for now, too?
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 stats package is entirely experimental as per top level package comment. I think it's fine to leave this here, since configures unstable OTel which lives in this package too.
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're going with that philosophy then why have experimental/stats
for the other stuff? Let's keep it together.
since configures unstable OTel which lives in this package too
OTel should not have anything in the stats package, 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.
K will move. I guess now I can move the MetricsRecorder interface to this experimental/stats/metrics.go file as well.
stats/metrics.go
Outdated
// Do not construct directly; use NewMetrics instead. | ||
type Metrics struct { | ||
// Metrics are the set of Metrics to initialize. | ||
Metrics map[Metric]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.
We should hide fields to prevent direct accesses, 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.
This field is read at OTel creation time. I could add a to slice function which would solve the function body comment below, but then it wouldn't be constant accesses at otel creation time when determining whether to build a metric or not. Let me see what looks best.
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 added a getter for this an unexported so we can use that map to check for presence in constant time.
stats/metrics.go
Outdated
|
||
// Join joins the metrics passed in with the metrics set, and returns a new copy | ||
// with the merged metrics. | ||
func (m *Metrics) Join(metrics *Metrics) *Metrics { // Use this 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.
Can this function body be simplified to m.Add(m.Values()/*or whatever*/)
?
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 left it as is since I added a getter for map only not to convert to slice.
experimental/stats/metricregistry.go
Outdated
|
||
// MetricDescriptor is the data for a registered metric. | ||
type MetricDescriptor struct { | ||
// The name. This name must be unique across whole binary (including any per |
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 name...of this metric"?
And "across the whole binary" reads a bit better.
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 for both.
experimental/stats/metricregistry.go
Outdated
// DefaultMetrics are the default metrics registered through global metrics | ||
// registry. This is written to at initialization time only, and is read only | ||
// after initialization. | ||
var DefaultMetrics = stats.NewMetrics() // loop through this set,, export metrisdescriptor passed upward for pointer and labels/optional lables... |
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.
Delete comment?
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.
Sorry missed this and a few others.
experimental/stats/metricregistry.go
Outdated
// is passed at the recording point in order to know which metric to record | ||
// on. | ||
type Int64CountHandle struct { | ||
MetricDescriptor *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.
Maybe you can point to a playground link illustrating the things you tried? The code as-is is still a struct so I can't see what you did.
experimental/stats/metricregistry.go
Outdated
// GetMetric returns the MetricDescriptor from the global registry. | ||
// | ||
// Returns nil if MetricDescriptor not present. | ||
func GetMetric(metric stats.Metric) *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.
GetMetricDescriptor
maybe?
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.
Or DescriptorForMetric
?
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.
Chose DescriptorForMetric.
Thanks for your help; got to all comments and fixed tests. |
// Whether this metric is on by default. | ||
Default 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.
Something to think about for later when you're implementing the things that actually use all of this: should we remove this field from the descriptor since it (I think?) only affects the initial registration and whether to put it into the DefaultMetrics
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.
Yeah I've thought about this persistence before, I wanted to delete this since this information is encoded in DefaultSet, but this is how the user registers through API now (since we persist a copy of what user sets). Do you see a way to delete the field but keep 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.
Two obvious other options:
RegisterFooMetric()
andRegisterDefaultFooMetric()
RegisterFooMetric(md MetricDescriptor, default bool)
I think this is okay, too, but we should consider the above since this data is effectively discarded immediately after registration.
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 rather do 2. Let's keep this in mind for the future since I'm sure this metrics registry will iterate organically as we merge more layers.
experimental/stats/metricregistry.go
Outdated
type Int64CountHandle MetricDescriptor | ||
|
||
// Record records the int64 count value on the metrics recorder provided. | ||
func (h *Int64CountHandle) Record(recorder MetricsRecorder, incr int64, labels ...string) { | ||
recorder.RecordIntCount(h, incr, 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.
Why does Int64CountHandle
have a size on it but the RecordIntCount
method does not? It seems like either both should say 64, or neither should.
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.
Ah yeah good point. I'd prefer leaving the 64 bit identifier as OpenTelemetry and Java/C specify 64. This is because OpenTelemetry has both int/float(32||64) instruments so if we scale this registry up in the future it more aligns with OpenTelemetry.
// NOTE: this function must only be called during initialization time (i.e. in | ||
// an init() function), and is not thread-safe. If multiple metrics are | ||
// registered with the same name, this function will panic. | ||
func RegisterInt64Count(descriptor MetricDescriptor) *Int64CountHandle { |
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.
Let's make sure the "numbers vs. no numbers" matches here too.
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.
See above comment; chose numbers route.
experimental/stats/metricregistry.go
Outdated
// MetricType is the type of metric. | ||
type MetricType int | ||
|
||
const ( | ||
MetricTypeIntCount MetricType = iota | ||
MetricTypeFloatCount | ||
MetricTypeIntHisto | ||
MetricTypeFloatHisto | ||
MetricTypeIntGauge | ||
) |
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.
Nit: this should maybe move to immediately before or after the MetricDescriptor
type definition.
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 point. Chose after since Type MetricType field is at the last field.
} | ||
RegisterInt64Count(desc) | ||
RegisterInt64Gauge(desc) | ||
} |
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 test doesn't fail if a panic doesn't occur.
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.
recover() returns nil if the test is not panicing. Then I typecast to string, and that won't contain the want right? Will my typecast of a nil to a string error?
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.
Oh, duh, the defer will still run, sorry!
experimental/stats/metrics.go
Outdated
} | ||
} | ||
|
||
// Metrics returns the metrics 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.
State that the returned map should be read-only and must not be modified, please.
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. It's weird since it says metrics set and returned map but I think those are aliases of each other, and functionally this does return a map, which is treated as a set.
This should be good to go modulo the small nits/test issue. Let's be ready to iterate on the registry stuff if you find it isn't working well while implementing the things that use it. |
This PR adds the instrument registry for Non Per Call Metrics. It also moves Metrics symbol to external, and defined a MetricRecorder type for non per call metrics.
The next PR will be the OpenTelemetry component usage of this global registry and implementation of MetricRecorder, including the merging of default metrics by shifting default metrics to a function as per Yashes suggestion.
RELEASE NOTES: N/A