Assume there is interface:
type CustomMetrics interface {
Update() error
Init() error
}
This interface will have many different implementations (structs), each implementation will be shipped with New
method, which returns new object of this type, here is example:
type TestMetric struct {
metric IntGauge
}
func (m *TestMetric) Init() error {
// registers the metric in otel metrics, basically deals with exporting it via HTTP
}
func (m *TestMetric) Update() error {
// Contacts another system, parses the data and update the metric value in otel metrics
}
the Update
methods are quite complex in reality. Also they are very different from one another in terms of implementation.
Following this pattern, if I want to instantiate all objects from a single function I will have to create a method similar to:
func InitAllMetrics() []CustomMetrics {
cm := make(CustomMetrics, 0)
testMetric1, err := NewTestMetric()
if err != nil {
return err
}
cm = append(cm, testMetric1)
testMetric2, err := NewTestMetric2()
if err != nil {
return err
}
cm = append(cm, testMetric2)
}
This method will become extremely big(I need to create around 50 metrics), it will be error prone to modify and difficult to test.
Are there any alternatives?
One would be using a table, like in tests:
func InitAllMetrics() ([]CustomMetrics, error) {
m := []func() (CustomMetrics, error){
func() (CustomMetrics, error) { return NewTestMetric1() },
func() (CustomMetrics, error) { return NewTestMetric2() },
}
cm := make([]CustomMetrics, len(m))
for i, mm := range m {
var err error
if cm[i], err = mm(); err != nil {
return nil, fmt.Errorf("error creating metrics %d: %w", i, err)
}
}
return cm, nil
}
This is one line per metric, it doesn't get much shorter than that.
Reflection is an option, but I'm not sure if it would be worth the effort.
An alternative with slightly worse error handling would be:
type metricsAggregator struct {
CustomMetrics []CustomMetrics
Err error
}
func (m *metricsAggregator) Add(cm CustomMetrics, err error) {
if err != nil {
if m.Err != nil {
m.Err = err
}
return
}
m.CustomMetrics = append(m.CustomMetrics, cm)
}
func InitAllMetrics() ([]CustomMetrics, error) {
var ma metricsAggregator
ma.Add(NewTestMetric1())
ma.Add(NewTestMetric2())
return ma.CustomMetrics, ma.Err
}
Or you use something like this in a loop:
func CreateTestMetric(i int) (CustomMetrics, error) {
switch i {
case 1:
return NewTestMetric1()
case 2:
return NewTestMetric2()
default:
return nil, fmt.Errorf("metric %d does not exist", i)
}
}
You get the idea. I'm unsure if it is worth thinking too long about it, since you're writing 50 NewTestMetric
functions anyway - maybe this is a concept worth thinking about.
Edit:
If you like the aggregator, you could initialize your metrics concurrently:
package ...
import (
"context"
"golang.org/x/sync/errgroup"
)
type metricsAggregator struct {
wg *errgroup.Group
ctx context.Context
cm chan []CustomMetrics
}
func NewMetricsAggregator(ctx context.Context, limit int) *metricsAggregator {
wg, ctx := errgroup.WithContext(ctx)
wg.SetLimit(limit)
cm := make(chan []CustomMetrics, 1)
cm <- nil
return &metricsAggregator{wg: wg, ctx: ctx, cm: cm}
}
func (m *metricsAggregator) Result() ([]CustomMetrics, error) {
if err := m.wg.Wait(); err != nil {
return nil, err
}
return <-m.cm, nil
}
func AddMetric[T CustomMetrics](m *metricsAggregator, newTestMetric func() (T, error)) {
m.wg.Go(func() error {
select {
case <-m.ctx.Done():
return m.ctx.Err()
default:
}
var testMetric CustomMetrics
var err error
if testMetric, err = newTestMetric(); err != nil {
return err
}
m.cm <- append(<-m.cm, testMetric)
return nil
})
}
func InitAllMetrics5(ctx context.Context) ([]CustomMetrics, error) {
ma := NewMetricsAggregator(ctx, 10)
AddMetric(ma, NewTestMetric1)
AddMetric(ma, NewTestMetric2)
return ma.Result()
}