-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Common metrics registry #1832
Comments
A thing that isn't mentioned in the above is that now that k6 has extensions, extensions also might need to emit metrics and currently, this is done like this and the metrics get defined in the same global way This and the connected issues more or less require that we break this and all extensions depending on it unless we want |
I think it's fine to break this since a) it was never documented, and b) this is why xk6 is still considered to be in alpha state ;) So I think we're in the clear to do changes like this that will ultimately result in a cleaner and safer API. |
Agree with @imiric, plus the breaking change won't be that big, especially compared to the module path change. IIRC, only a few extensions use custom metrics for now, so the sooner we make a better API for that, the better. |
I did look at the prometheus code and more accurately at the go client and the registry that is there as this seems to be used through the rest of the go code of prometheus. Some overview notes from what I understood:
Given the above, the direct usage of the prometheus code seems impossible without either big changes to it (defeating the point) or breaking changes to k6 ... On the other hand, the architecture looks interesting, but I really prefer if at least the first PR on this is less overwhelming and that if possible we don't emit metrics that won't be collected (see #1321 ). I will probably just prefer if we have a Registry that instead of registering stuff in ... creates the metrics and returns them and leave the emitting of the metrics as is (using the PushIfNotDone) at least for now. This will still let us have
|
What do we gain by having the registry create the metrics? Why not just have a So, instead of this: https://github.com/k6io/k6/blob/0e1f44192f799005f77c5eabe5ba84a6b724ef83/lib/metrics/metrics.go#L32-L36 Have something like this: type MetricsRegistry interface {
Get(name string) Metric
Register(name string, metric MEtric) error
}
type BuiltInMetrics struct {
VUs Gauge
VUsMax Gauge
Iterations Counter
IterationDuration Trend
// ...
}
func RegisterBuiltInMetrics(registry MetricsRegistry) (*BuiltInMetrics) {
builtin := &BuiltInMetrics{
VUs: NewGauge("vus"),
VUsMax: NewGauge("vus_max"),
Iterations: NewCounter("iteration_duration"),
IterationDuration: NewTrend("iteration_duration", true),
// ...
}
// This can probably be easily automated with some light reflection
registry.Register(builtin.VUs.Name(), &builtin.VUs)
registry.Register(builtin.VUsMax.Name(), &builtin.VUsMax)
// ...
return builtin
} This results in a type-safe struct that can be used internally in k6, without any globals (#572) or map lookups or locking, but also in a registry that has a Edit: if |
I don't see anything preventing us from just ... not registering something (in an extension for example) and emitting the metric. One of the things the prometheus architecture does is that the registry is how you gather the metrics and it only gathers from registered metrics so if you don't register your metrics they don't get gathered. Having the Registry be the place that has |
hmm yeah, good point 👍 and, thinking about it, it will also clean up my pseudo-code above, removing the need for repetition (or reflection): func RegisterBuiltInMetrics(registry MetricsRegistry) (*BuiltInMetrics) {
return &BuiltInMetrics{
VUs: registry.NewGauge("vus"),
VUsMax: registry.NewGauge("vus_max"),
Iterations: registry.NewCounter("iteration_duration"),
IterationDuration: registry.NewTrend("iteration_duration", true),
// ...
}
} |
Currently, k6 metrics (both built-in ones and custom ones), as well as any measurements that are made for them, are very decoupled from the places that consume them in the
Engine
(i.e. thresholds and the end-of-test summary and outputs). At a first glance, this might seem like a good architectural choice, but the reality is that instead of loosely coupled, they are very tightly coupled at a distance. This brings far more issues than it solves, if it even solves any... 😞With the current architecture, k6 doesn't "know" all of the built-in metrics that actually exist. They are defined as global variables in
lib/metrics/metrics.go
, but they are unconnected to anything else. And while there's a separate issue just to refactor that ugliness away (#572), not knowing the set of built-in metrics when the init context is executed and custom metrics can be defined makes it possible for users to define custom metrics with the same names as the built-in ones, but with different metric and value types.Compounding the issue, custom metrics themselves are quite problematic. There isn't absolutely any validation (#1435), so custom metrics from different VUs (or even the same VU) can redefine both built-in metrics and other custom metrics with different metric and value types! Currently, this is not going to throw an exception in k6:
The metrics constructor doesn't really do validation or create anything in a centralized place. It just returns something like a metric "template" that is attached to every metric
Sample
later generated by the JS.add()
method. So we don't really know the custom metrics and their types when theEngine
starts running the thresholds. It only realizes a metric exists when it "sees" a sample from it for the first time, and copies whatever "template" it first sees for the metric name 😭 https://github.com/loadimpact/k6/blob/46d53d99deba98add23079df93579408fa7ea094/core/engine.go#L406-L413So, this will not only not produce an error:
It will produce messed up summary like this:
The current architecture is blocking all of these issues and probably others as well:
I've opened this issue as the first prerequisite step for solving all of the ones listed above. I think the way to do that would be to have something like a single central "metrics registry" in k6. A list (or, rather, a map) of metric names with their types - both the built-in ones and all custom ones.
Any attempts at re-defining built-in or previously defined custom metrics should check the metric and value types of the new metric. If they don't match the already existing metric, an exception should be raised. And if they do match, instead of creating a new object, we should just return a
*Metric
pointer to the already existing metric object. To reduce undefined behavior even further, and to save us from having to ensure thread-safety, we should probably restrict the creation of custom metrics only to the initial init context (from which we get the exported scriptoptions
).The text was updated successfully, but these errors were encountered: