-
Notifications
You must be signed in to change notification settings - Fork 38
wip: refactor: uniquely identify metric time series by name and label sets #131
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -47,9 +47,14 @@ public final class PrometheusCollectorRegistry: Sendable { | |||||
} | ||||||
} | ||||||
|
||||||
private enum Metric { | ||||||
case counter(Counter, help: String) | ||||||
case counterWithLabels([String], [LabelsKey: Counter], help: String) | ||||||
private typealias Metric<T> = (metric: T, help: String) | ||||||
|
||||||
private struct MetricContainer<T> { | ||||||
var metrics: [LabelsKey: Metric<T>] | ||||||
} | ||||||
|
||||||
private enum MetricType { | ||||||
case counterMetrics(String, MetricContainer<Counter>) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general not sure yet about naming, ideas? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also I would love to benefit from your Swift expertise re below ... it's probably the Type? It should be typed out for each metric type?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah Type isn't right, this isn't a type, it's a container. Something feels off with the names here, this is a container but why do we need MetricContainer if it's a single field? By what is this supposed to be keyed, If you're seeing a |
||||||
case gauge(Gauge, help: String) | ||||||
case gaugeWithLabels([String], [LabelsKey: Gauge], help: String) | ||||||
case durationHistogram(DurationHistogram, help: String) | ||||||
|
@@ -58,7 +63,7 @@ public final class PrometheusCollectorRegistry: Sendable { | |||||
case valueHistogramWithLabels([String], [LabelsKey: ValueHistogram], [Double], help: String) | ||||||
} | ||||||
|
||||||
private let box = NIOLockedValueBox([String: Metric]()) | ||||||
private let box = NIOLockedValueBox([String: MetricType]()) | ||||||
|
||||||
/// Create a new collector registry | ||||||
public init() {} | ||||||
|
@@ -75,25 +80,7 @@ public final class PrometheusCollectorRegistry: Sendable { | |||||
/// If the parameter is omitted or an empty string is passed, the `# HELP` line will not be generated for this metric. | ||||||
/// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry`` | ||||||
public func makeCounter(name: String, help: String) -> Counter { | ||||||
let name = name.ensureValidMetricName() | ||||||
let help = help.ensureValidHelpText() | ||||||
return self.box.withLockedValue { store -> Counter in | ||||||
guard let value = store[name] else { | ||||||
let counter = Counter(name: name, labels: []) | ||||||
store[name] = .counter(counter, help: help) | ||||||
return counter | ||||||
} | ||||||
guard case .counter(let counter, _) = value else { | ||||||
fatalError( | ||||||
""" | ||||||
Could not make Counter with name: \(name), since another metric type | ||||||
already exists for the same name. | ||||||
""" | ||||||
) | ||||||
} | ||||||
|
||||||
return counter | ||||||
} | ||||||
return self.makeCounter(name: name, labels: [], help: help) | ||||||
} | ||||||
|
||||||
/// Creates a new ``Counter`` collector or returns the already existing one with the same name. | ||||||
|
@@ -104,7 +91,7 @@ public final class PrometheusCollectorRegistry: Sendable { | |||||
/// - Parameter name: A name to identify ``Counter``'s value. | ||||||
/// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry`` | ||||||
public func makeCounter(name: String) -> Counter { | ||||||
return self.makeCounter(name: name, help: "") | ||||||
return self.makeCounter(name: name, labels: [], help: "") | ||||||
} | ||||||
|
||||||
/// Creates a new ``Counter`` collector or returns the already existing one with the same name, | ||||||
|
@@ -116,7 +103,7 @@ public final class PrometheusCollectorRegistry: Sendable { | |||||
/// - Parameter descriptor: An ``MetricNameDescriptor`` that provides the fully qualified name for the metric. | ||||||
/// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry`` | ||||||
public func makeCounter(descriptor: MetricNameDescriptor) -> Counter { | ||||||
return self.makeCounter(name: descriptor.name, help: descriptor.helpText ?? "") | ||||||
return self.makeCounter(name: descriptor.name, labels: [], help: descriptor.helpText ?? "") | ||||||
} | ||||||
|
||||||
/// Creates a new ``Counter`` collector or returns the already existing one with the same name. | ||||||
|
@@ -131,51 +118,49 @@ public final class PrometheusCollectorRegistry: Sendable { | |||||
/// If the parameter is omitted or an empty string is passed, the `# HELP` line will not be generated for this metric. | ||||||
/// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry`` | ||||||
public func makeCounter(name: String, labels: [(String, String)], help: String) -> Counter { | ||||||
guard !labels.isEmpty else { | ||||||
return self.makeCounter(name: name, help: help) | ||||||
} | ||||||
|
||||||
let name = name.ensureValidMetricName() | ||||||
let labels = labels.ensureValidLabelNames() | ||||||
let help = help.ensureValidHelpText() | ||||||
let key = LabelsKey(labels) | ||||||
|
||||||
return self.box.withLockedValue { store -> Counter in | ||||||
guard let value = store[name] else { | ||||||
let labelNames = labels.allLabelNames | ||||||
guard let entry = store[name] else { | ||||||
// First time a Counter is registered with this name. | ||||||
let counter = Counter(name: name, labels: labels) | ||||||
|
||||||
store[name] = .counterWithLabels(labelNames, [LabelsKey(labels): counter], help: help) | ||||||
return counter | ||||||
} | ||||||
guard case .counterWithLabels(let labelNames, var dimensionLookup, let help) = value else { | ||||||
fatalError( | ||||||
""" | ||||||
Could not make Counter with name: \(name) and labels: \(labels), since another | ||||||
metric type already exists for the same name. | ||||||
""" | ||||||
let newMetric: Metric<Counter> = (metric: counter, help: help) | ||||||
let newContainer = MetricContainer<Counter>( | ||||||
metrics: [key: newMetric] | ||||||
) | ||||||
} | ||||||
|
||||||
let key = LabelsKey(labels) | ||||||
if let counter = dimensionLookup[key] { | ||||||
store[name] = .counterMetrics(name, newContainer) | ||||||
return counter | ||||||
} | ||||||
switch entry { | ||||||
case .counterMetrics(let existingName, var MetricContainer): | ||||||
if let existingMetric = MetricContainer.metrics[key] { | ||||||
return existingMetric.metric | ||||||
} | ||||||
|
||||||
// check if all labels match the already existing ones. | ||||||
if labelNames != labels.allLabelNames { | ||||||
// Even if the metric name is identical, each label set defines a unique time series | ||||||
let newCounter = Counter(name: name, labels: labels) | ||||||
let netNewMetric: Metric<Counter> = (metric: newCounter, help: help) | ||||||
MetricContainer.metrics[key] = netNewMetric | ||||||
|
||||||
// Write the modified entry back to the store. | ||||||
store[name] = .counterMetrics(existingName, MetricContainer) | ||||||
|
||||||
return newCounter | ||||||
|
||||||
default: | ||||||
// A metric with this name exists, but it's not a Counter. This is a programming error. | ||||||
// While Prometheus wouldn't stop you, it may result in unpredictable behavior with tools like Grafana or PromQL. | ||||||
fatalError( | ||||||
""" | ||||||
Could not make Counter with name: \(name) and labels: \(labels), since the | ||||||
label names don't match the label names of previously registered Counters with | ||||||
the same name. | ||||||
Metric type mismatch: | ||||||
Could not register a Counter with name '\(name)', | ||||||
since a different metric type (\(entry.self)) was already registered with this name. | ||||||
""" | ||||||
) | ||||||
} | ||||||
|
||||||
let counter = Counter(name: name, labels: labels) | ||||||
dimensionLookup[key] = counter | ||||||
store[name] = .counterWithLabels(labelNames, dimensionLookup, help: help) | ||||||
return counter | ||||||
} | ||||||
} | ||||||
|
||||||
|
@@ -703,17 +688,19 @@ public final class PrometheusCollectorRegistry: Sendable { | |||||
public func unregisterCounter(_ counter: Counter) { | ||||||
self.box.withLockedValue { store in | ||||||
switch store[counter.name] { | ||||||
case .counter(let storedCounter, _): | ||||||
guard storedCounter === counter else { return } | ||||||
store.removeValue(forKey: counter.name) | ||||||
case .counterWithLabels(let labelNames, var dimensions, let help): | ||||||
let labelsKey = LabelsKey(counter.labels) | ||||||
guard dimensions[labelsKey] === counter else { return } | ||||||
dimensions.removeValue(forKey: labelsKey) | ||||||
if dimensions.isEmpty { | ||||||
store.removeValue(forKey: counter.name) | ||||||
case .counterMetrics(let name, var MetricContainer): | ||||||
let key = LabelsKey(counter.labels) | ||||||
guard let existingMetric = MetricContainer.metrics[key], | ||||||
existingMetric.metric === counter | ||||||
else { | ||||||
return | ||||||
} | ||||||
MetricContainer.metrics.removeValue(forKey: key) | ||||||
|
||||||
if MetricContainer.metrics.isEmpty { | ||||||
store.removeValue(forKey: name) | ||||||
} else { | ||||||
store[counter.name] = .counterWithLabels(labelNames, dimensions, help: help) | ||||||
store[name] = .counterMetrics(name, MetricContainer) | ||||||
} | ||||||
default: | ||||||
return | ||||||
|
@@ -806,52 +793,52 @@ public final class PrometheusCollectorRegistry: Sendable { | |||||
let prefixHelp = "HELP" | ||||||
let prefixType = "TYPE" | ||||||
|
||||||
for (label, metric) in metrics { | ||||||
for (name, metric) in metrics { | ||||||
switch metric { | ||||||
case .counter(let counter, let help): | ||||||
help.isEmpty ? () : buffer.addLine(prefix: prefixHelp, name: label, value: help) | ||||||
buffer.addLine(prefix: prefixType, name: label, value: "counter") | ||||||
counter.emit(into: &buffer) | ||||||
|
||||||
case .counterWithLabels(_, let counters, let help): | ||||||
help.isEmpty ? () : buffer.addLine(prefix: prefixHelp, name: label, value: help) | ||||||
buffer.addLine(prefix: prefixType, name: label, value: "counter") | ||||||
for counter in counters.values { | ||||||
counter.emit(into: &buffer) | ||||||
case .counterMetrics(_, let MetricContainer): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure why the uppercase names?
Suggested change
|
||||||
// An entry should not be empty, as a safeguard skip if it is. | ||||||
guard let _ = MetricContainer.metrics.first?.value else { | ||||||
continue | ||||||
} | ||||||
for Metric in MetricContainer.metrics.values { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
let help = Metric.help | ||||||
help.isEmpty ? () : buffer.addLine(prefix: prefixHelp, name: name, value: help) | ||||||
buffer.addLine(prefix: prefixType, name: name, value: "counter") | ||||||
Metric.metric.emit(into: &buffer) | ||||||
} | ||||||
|
||||||
case .gauge(let gauge, let help): | ||||||
help.isEmpty ? () : buffer.addLine(prefix: prefixHelp, name: label, value: help) | ||||||
buffer.addLine(prefix: prefixType, name: label, value: "gauge") | ||||||
help.isEmpty ? () : buffer.addLine(prefix: prefixHelp, name: name, value: help) | ||||||
buffer.addLine(prefix: prefixType, name: name, value: "gauge") | ||||||
gauge.emit(into: &buffer) | ||||||
|
||||||
case .gaugeWithLabels(_, let gauges, let help): | ||||||
help.isEmpty ? () : buffer.addLine(prefix: prefixHelp, name: label, value: help) | ||||||
buffer.addLine(prefix: prefixType, name: label, value: "gauge") | ||||||
help.isEmpty ? () : buffer.addLine(prefix: prefixHelp, name: name, value: help) | ||||||
buffer.addLine(prefix: prefixType, name: name, value: "gauge") | ||||||
for gauge in gauges.values { | ||||||
gauge.emit(into: &buffer) | ||||||
} | ||||||
|
||||||
case .durationHistogram(let histogram, let help): | ||||||
help.isEmpty ? () : buffer.addLine(prefix: prefixHelp, name: label, value: help) | ||||||
buffer.addLine(prefix: prefixType, name: label, value: "histogram") | ||||||
help.isEmpty ? () : buffer.addLine(prefix: prefixHelp, name: name, value: help) | ||||||
buffer.addLine(prefix: prefixType, name: name, value: "histogram") | ||||||
histogram.emit(into: &buffer) | ||||||
|
||||||
case .durationHistogramWithLabels(_, let histograms, _, let help): | ||||||
help.isEmpty ? () : buffer.addLine(prefix: prefixHelp, name: label, value: help) | ||||||
buffer.addLine(prefix: prefixType, name: label, value: "histogram") | ||||||
help.isEmpty ? () : buffer.addLine(prefix: prefixHelp, name: name, value: help) | ||||||
buffer.addLine(prefix: prefixType, name: name, value: "histogram") | ||||||
for histogram in histograms.values { | ||||||
histogram.emit(into: &buffer) | ||||||
} | ||||||
|
||||||
case .valueHistogram(let histogram, let help): | ||||||
help.isEmpty ? () : buffer.addLine(prefix: prefixHelp, name: label, value: help) | ||||||
buffer.addLine(prefix: prefixType, name: label, value: "histogram") | ||||||
help.isEmpty ? () : buffer.addLine(prefix: prefixHelp, name: name, value: help) | ||||||
buffer.addLine(prefix: prefixType, name: name, value: "histogram") | ||||||
histogram.emit(into: &buffer) | ||||||
|
||||||
case .valueHistogramWithLabels(_, let histograms, _, let help): | ||||||
help.isEmpty ? () : buffer.addLine(prefix: prefixHelp, name: label, value: help) | ||||||
buffer.addLine(prefix: prefixType, name: label, value: "histogram") | ||||||
help.isEmpty ? () : buffer.addLine(prefix: prefixHelp, name: name, value: help) | ||||||
buffer.addLine(prefix: prefixType, name: name, value: "histogram") | ||||||
for histogram in histograms.values { | ||||||
histogram.emit(into: &buffer) | ||||||
} | ||||||
|
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 avoid tuples when a thing seems more like a normal data type, make it a real type I think