Skip to content

Consider not having channels in the Collector interface #228

@beorn7

Description

@beorn7

This is the current Collector interface:

type Collector interface {
    Describe(chan<- *Desc)
    Collect(chan<- Metric)
}

The main reason for having channels there was that back in 2013, channels were cool, so I was primarily looking for neat ways to make use of them rather than avoiding them in APIs. While the current interface has some merits, the pattern used here is very unconventional, and now that breaking changes will happen anyway, we have the chance of changing the interface. In the following, I'll list pros and cons to come to an informed decision.

The new interface would look like:

type Collector interface {
    Describe() []*Desc
    Collect() []Metric
}

Reasons for changing the interface

  • Channels in APIs are generally frowned upon in the community. Users get irritated just by looking at it. The usefulness of a chan API needs to outweigh the amount of irritation.
  • Channels suggest that concurrency is part of the game, which is only partially true here.
  • Performance/allocation penalty of the slice interface is not overly relevant as registration and collection happens relatively rare. (Exception would be high-frequency scrape scenarios.)

Reasons for keeping the interface as is

  • This would be one of the more invasive changes. Many code lines have to be changed by those who implemented custom collectors, see code examples below. The benefits of changing the interface need to be convincing enough to justify this work.
  • The code for chaining collectors is quite neat, see below.
  • Fewer slice juggling required, has an advantage in terms of allocations and code complexity.
  • Concurrent Metric collection (as it is used by the registry) is more straight-forward with the channel being passed around. Also, metrics are already encoded into protobufs concurrently with the collection still running (which probably only matters with really large scrapes).
  • Only "power users" get in touch with the Collector interface, which limits the damage of the unusual API.

Appendix: Code examples

Sequential collection with old interface

func (c *C) Collect(m chan<- prometheus.Metric) {
    m <- c.constMetric
    c.subCollector.Collect(m)
    c.anotherSubCollector.Collect(m)
}

Sequential collection with new interface

func (c *C) Collect() []prometheus.Metric {
    var m []prometheus.Metric
    m = append(m, c.constMetric)
    m = append(m, c.subCollector.Collect()...)
    m = append(m, c.anotherSubCollector.Collect(m)...)
    return m
}

or more compact (but arguably also more confusing):

func (c *C) Collect() []prometheus.Metric {
    m := []promtheus.Metric{c.constMetric}
    m = append(m, c.subCollector.Collect()...)
    return append(m, c.anotherSubCollector.Collect(m)...)
}

Concurrent collection with old interface

func (c *C) Collect(m chan<- prometheus.Metric) {
    var wg sync.WaitGroup
    wg.Add(len(c.colls))
    for _, coll := range c.colls {
        go func(coll prometheus.Collector) {
            coll.Collect(m) // Protobuf encoding already starting here.
            wg.Done()
        }(coll)
    }
    wg.Wait()
}

Concurrent collection with new interface

func (c *ApacheCollector) Collect() []prometheus.Metric {
    ch := make(chan []prometheus.Metric)
    for _, coll := range c.colls {
        go func(coll prometheus.Collector) {
            ch <- coll.Collect()
        }(coll)
    }
    var m []prometheus.Metric
    for _ = range c.colls {
        m = append(m, <-ch...)
    }
    return m // Only now can the registry start encoding.
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions