-3

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?

1
  • Yes, code generation, reflection or some kind of registry.
    – Volker
    Commented Jun 26 at 20:06

1 Answer 1

0

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()
}
1
  • 1
    I will go with the aggregate way, since the only side effect of the error I want is to not add it in the slice & some logging. Then mocking the Add function to error on specific metrics should allow me to test the behavior of the system when errors happen. Thanks! Commented Jun 27 at 15:06

Not the answer you're looking for? Browse other questions tagged or ask your own question.