diff --git a/Sources/Prometheus/MetricDescriptor.swift b/Sources/Prometheus/MetricDescriptor.swift index 577c1fa..9f73439 100644 --- a/Sources/Prometheus/MetricDescriptor.swift +++ b/Sources/Prometheus/MetricDescriptor.swift @@ -27,27 +27,25 @@ public struct MetricNameDescriptor { /// The required, descriptive base name of the metric. public let metricName: String - /// An optional suffix describing the metric's unit (e.g., `total`). - public let unitName: String? + /// The required suffix describing the metric's unit (e.g., `total`). + public let unitName: String - /// Optional help text for the metric. If a non-empty string is provided, it will be emitted as a `# HELP` line in the exposition format. - /// If the parameter is omitted or an empty string is passed, the `# HELP` line will not be generated for this metric. - public let helpText: String? + /// The required help text for the metric. If a non-empty string is provided, it will be emitted as a `# HELP` line in the exposition format. + public let helpText: String /// Creates a new ``MetricNameDescriptor`` that defines the components of a fully qualified Prometheus metric name. /// /// - Parameter namespace: An optional top-level namespace for the metric. /// - Parameter subsystem: An optional subsystem to group related metrics within a namespace. /// - Parameter metricName: The required, descriptive base name of the metric. - /// - Parameter unitName: An optional suffix describing the metric's unit (e.g., `total`). - /// - Parameter helpText: Optional help text for the metric. If a non-empty string is provided, it will be emitted as a `# HELP` line in the exposition format. - /// If the parameter is omitted or an empty string is passed, the `# HELP` line will not be generated for this metric. + /// - Parameter unitName: The required suffix describing the metric's unit (e.g., `total`). + /// - Parameter helpText: The required help text for the metric. If a non-empty string is provided, it will be emitted as a `# HELP` line in the exposition format. public init( namespace: String? = nil, subsystem: String? = nil, metricName: String, - unitName: String? = nil, - helpText: String? = nil + unitName: String, + helpText: String ) { precondition(!metricName.isEmpty, "metricName must not be empty") self.namespace = namespace @@ -64,3 +62,36 @@ public struct MetricNameDescriptor { .joined(separator: "_") } } + +// MARK: - Deprecated + +extension MetricNameDescriptor { + /// Creates a new ``MetricNameDescriptor`` that defines the components of a fully qualified Prometheus metric name. + /// + /// - Parameter namespace: An optional top-level namespace for the metric. + /// - Parameter subsystem: An optional subsystem to group related metrics within a namespace. + /// - Parameter metricName: The required, descriptive base name of the metric. + /// - Parameter unitName: An optional suffix describing the metric's unit (e.g., `total`). + /// - Parameter helpText: Optional help text for the metric. If a non-empty string is provided, it will be emitted as a `# HELP` line in the exposition format. + /// If the parameter is omitted or an empty string is passed, the `# HELP` line will not be generated for this metric. + @available( + *, + deprecated, + message: "This initializer is deprecated; 'unitName' and 'helpText' are now required parameters." + ) + public init( + namespace: String? = nil, + subsystem: String? = nil, + metricName: String, + unitName: String? = nil, + helpText: String? = nil + ) { + self.init( + namespace: namespace, + subsystem: subsystem, + metricName: metricName, + unitName: unitName ?? "", + helpText: helpText ?? "" + ) + } +} diff --git a/Sources/Prometheus/PrometheusCollectorRegistry.swift b/Sources/Prometheus/PrometheusCollectorRegistry.swift index beb59b2..8343647 100644 --- a/Sources/Prometheus/PrometheusCollectorRegistry.swift +++ b/Sources/Prometheus/PrometheusCollectorRegistry.swift @@ -213,8 +213,9 @@ public final class PrometheusCollectorRegistry: Sendable { /// - Parameter help: Help text for the metric. If a non-empty string is provided, it will be emitted as a `# HELP` line in the exposition format. /// 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`` + @available(*, deprecated, message: "Use `makeCounter(descriptor:)` instead.") public func makeCounter(name: String, help: String) -> Counter { - return self.makeCounter(name: name, labels: [], help: help) + return self._makeCounter(name: name, labels: [], help: help) } /// Creates a new ``Counter`` collector or returns the already existing one with the same name. @@ -224,8 +225,9 @@ public final class PrometheusCollectorRegistry: Sendable { /// /// - Parameter name: A name to identify ``Counter``'s value. /// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry`` + @available(*, deprecated, message: "Use `makeCounter(descriptor:)` instead.") public func makeCounter(name: String) -> Counter { - return self.makeCounter(name: name, labels: [], help: "") + return self._makeCounter(name: name, labels: [], help: "") } /// Creates a new ``Counter`` collector or returns the already existing one with the same name, @@ -237,7 +239,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, labels: [], 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. @@ -251,66 +253,9 @@ public final class PrometheusCollectorRegistry: Sendable { /// - Parameter help: Help text for the metric. If a non-empty string is provided, it will be emitted as a `# HELP` line in the exposition format. /// 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`` + @available(*, deprecated, message: "Use `makeCounter(descriptor:labels:)` instead.") public func makeCounter(name: String, labels: [(String, String)], help: String) -> Counter { - 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 entry = store[name] else { - // First time a Counter is registered with this name. - let counter = Counter(name: name, labels: labels) - let counterFamily = MetricFamily( - metricsByLabelSets: labels.isEmpty ? [:] : [key: counter], - help: help, - metricUnlabeled: labels.isEmpty ? counter : nil - ) - store[name] = .counter(counterFamily) - return counter - } - - switch entry { - case .counter(let existingCounterFamily): - - // Validate help text consistency. While Prometheus wouldn't break with duplicate and distinct - // HELP lines, the client enforces uniqueness and consistency. - if let existingHelp = existingCounterFamily.help, existingHelp != help { - fatalError( - """ - Help text mismatch for metric '\(name)': - Existing help: '\(existingHelp)' - New help: '\(help)' - All metrics with the same name must have identical help text. - """ - ) - } - - if let existingCounter = existingCounterFamily.getMetric(for: labels) { - return existingCounter - } - - // Even if the metric name is identical, each label set defines a unique time series. - let counter = Counter(name: name, labels: labels) - let updatedFamily = existingCounterFamily.adding(metric: counter, for: labels) - - // Write the modified entry back to the store. - store[name] = .counter(updatedFamily) - - return counter - - 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( - """ - Metric type mismatch: - Could not register a Counter with name '\(name)', - since a different metric type (\(entry.self)) was already registered with this name. - """ - ) - } - } + return self._makeCounter(name: name, labels: labels, help: help) } /// Creates a new ``Counter`` collector or returns the already existing one with the same name. @@ -322,8 +267,9 @@ public final class PrometheusCollectorRegistry: Sendable { /// - Parameter labels: Labels are sets of key-value pairs that allow us to characterize and organize /// what’s actually being measured in a Prometheus metric. /// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry`` + @available(*, deprecated, message: "Use `makeCounter(descriptor:labels:)` instead.") public func makeCounter(name: String, labels: [(String, String)]) -> Counter { - return self.makeCounter(name: name, labels: labels, help: "") + return self._makeCounter(name: name, labels: labels, help: "") } /// Creates a new ``Counter`` collector or returns the already existing one with the same name, @@ -337,7 +283,7 @@ public final class PrometheusCollectorRegistry: Sendable { /// what’s actually being measured in a Prometheus metric. /// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry`` public func makeCounter(descriptor: MetricNameDescriptor, labels: [(String, String)]) -> Counter { - return self.makeCounter(name: descriptor.name, labels: labels, help: descriptor.helpText ?? "") + return self._makeCounter(name: descriptor.name, labels: labels, help: descriptor.helpText) } /// Creates a new ``Gauge`` collector or returns the already existing one with the same name. @@ -349,8 +295,9 @@ public final class PrometheusCollectorRegistry: Sendable { /// - Parameter help: Help text for the metric. If a non-empty string is provided, it will be emitted as a `# HELP` line in the exposition format. /// If the parameter is omitted or an empty string is passed, the `# HELP` line will not be generated for this metric. /// - Returns: A ``Gauge`` that is registered with this ``PrometheusCollectorRegistry`` + @available(*, deprecated, message: "Use `makeGauge(descriptor:)` instead.") public func makeGauge(name: String, help: String) -> Gauge { - return self.makeGauge(name: name, labels: [], help: help) + return self._makeGauge(name: name, labels: [], help: help) } /// Creates a new ``Gauge`` collector or returns the already existing one with the same name. @@ -360,8 +307,9 @@ public final class PrometheusCollectorRegistry: Sendable { /// /// - Parameter name: A name to identify ``Gauge``'s value. /// - Returns: A ``Gauge`` that is registered with this ``PrometheusCollectorRegistry`` + @available(*, deprecated, message: "Use `makeGauge(descriptor:)` instead.") public func makeGauge(name: String) -> Gauge { - return self.makeGauge(name: name, labels: [], help: "") + return self._makeGauge(name: name, labels: [], help: "") } /// Creates a new ``Gauge`` collector or returns the already existing one with the same name, @@ -373,7 +321,7 @@ public final class PrometheusCollectorRegistry: Sendable { /// - Parameter descriptor: An ``MetricNameDescriptor`` that provides the fully qualified name for the metric. /// - Returns: A ``Gauge`` that is registered with this ``PrometheusCollectorRegistry`` public func makeGauge(descriptor: MetricNameDescriptor) -> Gauge { - return self.makeGauge(name: descriptor.name, labels: [], help: descriptor.helpText ?? "") + return self._makeGauge(name: descriptor.name, labels: [], help: descriptor.helpText) } /// Creates a new ``Gauge`` collector or returns the already existing one with the same name. @@ -387,66 +335,9 @@ public final class PrometheusCollectorRegistry: Sendable { /// - Parameter help: Help text for the metric. If a non-empty string is provided, it will be emitted as a `# HELP` line in the exposition format. /// If the parameter is omitted or an empty string is passed, the `# HELP` line will not be generated for this metric. /// - Returns: A ``Gauge`` that is registered with this ``PrometheusCollectorRegistry`` + @available(*, deprecated, message: "Use `makeGauge(descriptor:labels:)` instead.") public func makeGauge(name: String, labels: [(String, String)], help: String) -> Gauge { - let name = name.ensureValidMetricName() - let labels = labels.ensureValidLabelNames() - let help = help.ensureValidHelpText() - let key = LabelsKey(labels) - - return self.box.withLockedValue { store -> Gauge in - guard let entry = store[name] else { - // First time a Gauge is registered with this name. - let gauge = Gauge(name: name, labels: labels) - let gaugeFamily = MetricFamily( - metricsByLabelSets: labels.isEmpty ? [:] : [key: gauge], - help: help, - metricUnlabeled: labels.isEmpty ? gauge : nil - ) - store[name] = .gauge(gaugeFamily) - return gauge - } - - switch entry { - case .gauge(let existingGaugeFamily): - - // Validate help text consistency. While Prometheus wouldn't break with duplicate and distinct - // HELP lines, the client enforces uniqueness and consistency. - if let existingHelp = existingGaugeFamily.help, existingHelp != help { - fatalError( - """ - Help text mismatch for metric '\(name)': - Existing help: '\(existingHelp)' - New help: '\(help)' - All metrics with the same name must have identical help text. - """ - ) - } - - if let existingGauge = existingGaugeFamily.getMetric(for: labels) { - return existingGauge - } - - // Even if the metric name is identical, each label set defines a unique time series. - let gauge = Gauge(name: name, labels: labels) - let updatedFamily = existingGaugeFamily.adding(metric: gauge, for: labels) - - // Write the modified entry back to the store. - store[name] = .gauge(updatedFamily) - - return gauge - - default: - // A metric with this name exists, but it's not a Gauge. This is a programming error. - // While Prometheus wouldn't stop you, it may result in unpredictable behavior with tools like Grafana or PromQL. - fatalError( - """ - Metric type mismatch: - Could not register a Gauge with name '\(name)', - since a different metric type (\(entry.self)) was already registered with this name. - """ - ) - } - } + return self._makeGauge(name: name, labels: labels, help: help) } /// Creates a new ``Gauge`` collector or returns the already existing one with the same name. @@ -458,8 +349,9 @@ public final class PrometheusCollectorRegistry: Sendable { /// - Parameter labels: Labels are sets of key-value pairs that allow us to characterize and organize /// what’s actually being measured in a Prometheus metric. /// - Returns: A ``Gauge`` that is registered with this ``PrometheusCollectorRegistry`` + @available(*, deprecated, message: "Use `makeGauge(descriptor:labels:)` instead.") public func makeGauge(name: String, labels: [(String, String)]) -> Gauge { - return self.makeGauge(name: name, labels: labels, help: "") + return self._makeGauge(name: name, labels: labels, help: "") } /// Creates a new ``Gauge`` collector or returns the already existing one with the same name and labels, @@ -473,7 +365,7 @@ public final class PrometheusCollectorRegistry: Sendable { /// what’s actually being measured in a Prometheus metric. /// - Returns: A ``Gauge`` that is registered with this ``PrometheusCollectorRegistry`` public func makeGauge(descriptor: MetricNameDescriptor, labels: [(String, String)]) -> Gauge { - return self.makeGauge(name: descriptor.name, labels: labels, help: descriptor.helpText ?? "") + return self._makeGauge(name: descriptor.name, labels: labels, help: descriptor.helpText) } /// Creates a new ``DurationHistogram`` collector or returns the already existing one with the same name. @@ -486,8 +378,9 @@ public final class PrometheusCollectorRegistry: Sendable { /// - Parameter help: Help text for the metric. If a non-empty string is provided, it will be emitted as a `# HELP` line in the exposition format. /// If the parameter is omitted or an empty string is passed, the `# HELP` line will not be generated for this metric. /// - Returns: A ``DurationHistogram`` that is registered with this ``PrometheusCollectorRegistry`` + @available(*, deprecated, message: "Use `makeDurationHistogram(descriptor:buckets:)` instead.") public func makeDurationHistogram(name: String, buckets: [Duration], help: String) -> DurationHistogram { - return self.makeDurationHistogram(name: name, labels: [], buckets: buckets, help: help) + return self._makeDurationHistogram(name: name, labels: [], buckets: buckets, help: help) } /// Creates a new ``DurationHistogram`` collector or returns the already existing one with the same name. @@ -498,8 +391,9 @@ public final class PrometheusCollectorRegistry: Sendable { /// - Parameter name: A name to identify ``DurationHistogram``'s value. /// - Parameter buckets: Define the buckets that shall be used within the ``DurationHistogram`` /// - Returns: A ``DurationHistogram`` that is registered with this ``PrometheusCollectorRegistry`` + @available(*, deprecated, message: "Use `makeDurationHistogram(descriptor:buckets:)` instead.") public func makeDurationHistogram(name: String, buckets: [Duration]) -> DurationHistogram { - return self.makeDurationHistogram(name: name, labels: [], buckets: buckets, help: "") + return self._makeDurationHistogram(name: name, labels: [], buckets: buckets, help: "") } /// Creates a new ``DurationHistogram`` collector or returns the already existing one with the same name, @@ -512,11 +406,11 @@ public final class PrometheusCollectorRegistry: Sendable { /// - Parameter buckets: Define the buckets that shall be used within the ``DurationHistogram`` /// - Returns: A ``DurationHistogram`` that is registered with this ``PrometheusCollectorRegistry`` public func makeDurationHistogram(descriptor: MetricNameDescriptor, buckets: [Duration]) -> DurationHistogram { - return self.makeDurationHistogram( + return self._makeDurationHistogram( name: descriptor.name, labels: [], buckets: buckets, - help: descriptor.helpText ?? "" + help: descriptor.helpText ) } @@ -532,86 +426,19 @@ public final class PrometheusCollectorRegistry: Sendable { /// - Parameter help: Help text for the metric. If a non-empty string is provided, it will be emitted as a `# HELP` line in the exposition format. /// If the parameter is omitted or an empty string is passed, the `# HELP` line will not be generated for this metric. /// - Returns: A ``DurationHistogram`` that is registered with this ``PrometheusCollectorRegistry`` + @available(*, deprecated, message: "Use `makeDurationHistogram(descriptor:labels:buckets:)` instead.") public func makeDurationHistogram( name: String, labels: [(String, String)], buckets: [Duration], help: String ) -> DurationHistogram { - let name = name.ensureValidMetricName() - let labels = labels.ensureValidLabelNames() - let help = help.ensureValidHelpText() - let key = LabelsKey(labels) - - return self.box.withLockedValue { store -> DurationHistogram in - guard let entry = store[name] else { - // First time a DurationHistogram is registered with this name. This defines the buckets. - let histogram = DurationHistogram(name: name, labels: labels, buckets: buckets) - let histogramFamily = MetricFamily( - metricsByLabelSets: labels.isEmpty ? [:] : [key: histogram], - buckets: .duration(buckets), - help: help, - metricUnlabeled: labels.isEmpty ? histogram : nil - ) - store[name] = .durationHistogram(histogramFamily) - return histogram - } - - switch entry { - case .durationHistogram(let existingHistogramFamily): - - // Validate help text consistency. While Prometheus wouldn't break with duplicate and distinct - // HELP lines, the client enforces uniqueness and consistency. - if let existingHelp = existingHistogramFamily.help, existingHelp != help { - fatalError( - """ - Help text mismatch for metric '\(name)': - Existing help: '\(existingHelp)' - New help: '\(help)' - All metrics with the same name must have identical help text. - """ - ) - } - - // Validate buckets match the stored ones. - if case .duration(let storedBuckets) = existingHistogramFamily.buckets { - guard storedBuckets == buckets else { - fatalError( - """ - Bucket mismatch for DurationHistogram '\(name)': - Expected buckets: \(storedBuckets) - Provided buckets: \(buckets) - All metrics with the same name must use identical buckets. - """ - ) - } - } - - if let existingHistogram = existingHistogramFamily.getMetric(for: labels) { - return existingHistogram - } - - // Even if the metric name is identical, each label set defines a unique time series. - let histogram = DurationHistogram(name: name, labels: labels, buckets: buckets) - let updatedFamily = existingHistogramFamily.adding(metric: histogram, for: labels) - - // Write the modified entry back to the store. - store[name] = .durationHistogram(updatedFamily) - - return histogram - - default: - // A metric with this name exists, but it's not a DurationHistogram. This is a programming error. - // While Prometheus wouldn't stop you, it may result in unpredictable behavior with tools like Grafana or PromQL. - fatalError( - """ - Metric type mismatch: - Could not register a DurationHistogram with name '\(name)', - since a different metric type (\(entry.self)) was already registered with this name. - """ - ) - } - } + return self._makeDurationHistogram( + name: name, + labels: labels, + buckets: buckets, + help: help + ) } /// Creates a new ``DurationHistogram`` collector or returns the already existing one with the same name. @@ -624,12 +451,13 @@ public final class PrometheusCollectorRegistry: Sendable { /// what’s actually being measured in a Prometheus metric. /// - Parameter buckets: Define the buckets that shall be used within the ``DurationHistogram`` /// - Returns: A ``DurationHistogram`` that is registered with this ``PrometheusCollectorRegistry`` + @available(*, deprecated, message: "Use `makeDurationHistogram(descriptor:labels:buckets:)` instead.") public func makeDurationHistogram( name: String, labels: [(String, String)], buckets: [Duration] ) -> DurationHistogram { - return self.makeDurationHistogram( + return self._makeDurationHistogram( name: name, labels: labels, buckets: buckets, @@ -653,11 +481,11 @@ public final class PrometheusCollectorRegistry: Sendable { labels: [(String, String)], buckets: [Duration] ) -> DurationHistogram { - return self.makeDurationHistogram( + return self._makeDurationHistogram( name: descriptor.name, labels: labels, buckets: buckets, - help: descriptor.helpText ?? "" + help: descriptor.helpText ) } @@ -671,8 +499,9 @@ public final class PrometheusCollectorRegistry: Sendable { /// - Parameter help: Help text for the metric. If a non-empty string is provided, it will be emitted as a `# HELP` line in the exposition format. /// If the parameter is omitted or an empty string is passed, the `# HELP` line will not be generated for this metric. /// - Returns: A ``ValueHistogram`` that is registered with this ``PrometheusCollectorRegistry`` + @available(*, deprecated, message: "Use `makeValueHistogram(descriptor:buckets:)` instead.") public func makeValueHistogram(name: String, buckets: [Double], help: String) -> ValueHistogram { - return self.makeValueHistogram(name: name, labels: [], buckets: buckets, help: help) + return self._makeValueHistogram(name: name, labels: [], buckets: buckets, help: help) } /// Creates a new ``ValueHistogram`` collector or returns the already existing one with the same name. @@ -683,8 +512,9 @@ public final class PrometheusCollectorRegistry: Sendable { /// - Parameter name: A name to identify ``ValueHistogram``'s value. /// - Parameter buckets: Define the buckets that shall be used within the ``ValueHistogram`` /// - Returns: A ``ValueHistogram`` that is registered with this ``PrometheusCollectorRegistry`` + @available(*, deprecated, message: "Use `makeValueHistogram(descriptor:buckets:)` instead.") public func makeValueHistogram(name: String, buckets: [Double]) -> ValueHistogram { - return self.makeValueHistogram(name: name, labels: [], buckets: buckets, help: "") + return self._makeValueHistogram(name: name, labels: [], buckets: buckets, help: "") } /// Creates a new ``ValueHistogram`` collector or returns the already existing one with the same name, @@ -697,11 +527,11 @@ public final class PrometheusCollectorRegistry: Sendable { /// - Parameter buckets: Define the buckets that shall be used within the ``ValueHistogram`` /// - Returns: A ``ValueHistogram`` that is registered with this ``PrometheusCollectorRegistry`` public func makeValueHistogram(descriptor: MetricNameDescriptor, buckets: [Double]) -> ValueHistogram { - return self.makeValueHistogram( + return self._makeValueHistogram( name: descriptor.name, labels: [], buckets: buckets, - help: descriptor.helpText ?? "" + help: descriptor.helpText ) } @@ -717,7 +547,278 @@ public final class PrometheusCollectorRegistry: Sendable { /// - Parameter help: Help text for the metric. If a non-empty string is provided, it will be emitted as a `# HELP` line in the exposition format. /// If the parameter is omitted or an empty string is passed, the `# HELP` line will not be generated for this metric. /// - Returns: A ``ValueHistogram`` that is registered with this ``PrometheusCollectorRegistry`` + @available(*, deprecated, message: "Use `makeValueHistogram(descriptor:labels:buckets:)` instead.") + public func makeValueHistogram( + name: String, + labels: [(String, String)], + buckets: [Double], + help: String + ) -> ValueHistogram { + return self._makeValueHistogram( + name: name, + labels: labels, + buckets: buckets, + help: help + ) + } + + /// Creates a new ``ValueHistogram`` collector or returns the already existing one with the same name. + /// + /// When the ``PrometheusCollectorRegistry/emit(into:)`` is called, metrics from the + /// created ``ValueHistogram`` will be part of the export. + /// + /// - Parameter name: A name to identify ``ValueHistogram``'s value. + /// - Parameter labels: Labels are sets of key-value pairs that allow us to characterize and organize + /// what’s actually being measured in a Prometheus metric. + /// - Parameter buckets: Define the buckets that shall be used within the ``ValueHistogram`` + /// - Returns: A ``ValueHistogram`` that is registered with this ``PrometheusCollectorRegistry`` + @available(*, deprecated, message: "Use `makeValueHistogram(descriptor:labels:buckets:)` instead.") + public func makeValueHistogram( + name: String, + labels: [(String, String)], + buckets: [Double] + ) -> ValueHistogram { + return self._makeValueHistogram( + name: name, + labels: labels, + buckets: buckets, + help: "" + ) + } + + /// Creates a new ``ValueHistogram`` collector or returns the already existing one with the same name and labels, + /// based on the provided descriptor. + /// + /// When the ``PrometheusCollectorRegistry/emit(into:)`` is called, metrics from the + /// created ``ValueHistogram`` will be part of the export. + /// + /// - Parameter descriptor: An ``MetricNameDescriptor`` that provides the fully qualified name for the metric. + /// - Parameter labels: Labels are sets of key-value pairs that allow us to characterize and organize + /// what’s actually being measured in a Prometheus metric. + /// - Parameter buckets: Define the buckets that shall be used within the ``ValueHistogram`` + /// - Returns: A ``ValueHistogram`` that is registered with this ``PrometheusCollectorRegistry`` public func makeValueHistogram( + descriptor: MetricNameDescriptor, + labels: [(String, String)], + buckets: [Double] + ) -> ValueHistogram { + return self._makeValueHistogram( + name: descriptor.name, + labels: labels, + buckets: buckets, + help: descriptor.helpText + ) + } + + // MARK: - Private Implementation + + private func _makeCounter(name: String, labels: [(String, String)], help: String) -> Counter { + 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 entry = store[name] else { + // First time a Counter is registered with this name. + let counter = Counter(name: name, labels: labels) + let counterFamily = MetricFamily( + metricsByLabelSets: labels.isEmpty ? [:] : [key: counter], + help: help, + metricUnlabeled: labels.isEmpty ? counter : nil + ) + store[name] = .counter(counterFamily) + return counter + } + + switch entry { + case .counter(let existingCounterFamily): + + // Validate help text consistency. While Prometheus wouldn't break with duplicate and distinct + // HELP lines, the client enforces uniqueness and consistency. + if let existingHelp = existingCounterFamily.help, existingHelp != help { + fatalError( + """ + Help text mismatch for metric '\(name)': + Existing help: '\(existingHelp)' + New help: '\(help)' + All metrics with the same name must have identical help text. + """ + ) + } + + if let existingCounter = existingCounterFamily.getMetric(for: labels) { + return existingCounter + } + + // Even if the metric name is identical, each label set defines a unique time series. + let counter = Counter(name: name, labels: labels) + let updatedFamily = existingCounterFamily.adding(metric: counter, for: labels) + + // Write the modified entry back to the store. + store[name] = .counter(updatedFamily) + + return counter + + 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( + """ + Metric type mismatch: + Could not register a Counter with name '\(name)', + since a different metric type (\(entry.self)) was already registered with this name. + """ + ) + } + } + } + + private func _makeGauge(name: String, labels: [(String, String)], help: String) -> Gauge { + let name = name.ensureValidMetricName() + let labels = labels.ensureValidLabelNames() + let help = help.ensureValidHelpText() + let key = LabelsKey(labels) + + return self.box.withLockedValue { store -> Gauge in + guard let entry = store[name] else { + // First time a Gauge is registered with this name. + let gauge = Gauge(name: name, labels: labels) + let gaugeFamily = MetricFamily( + metricsByLabelSets: labels.isEmpty ? [:] : [key: gauge], + help: help, + metricUnlabeled: labels.isEmpty ? gauge : nil + ) + store[name] = .gauge(gaugeFamily) + return gauge + } + + switch entry { + case .gauge(let existingGaugeFamily): + + // Validate help text consistency. While Prometheus wouldn't break with duplicate and distinct + // HELP lines, the client enforces uniqueness and consistency. + if let existingHelp = existingGaugeFamily.help, existingHelp != help { + fatalError( + """ + Help text mismatch for metric '\(name)': + Existing help: '\(existingHelp)' + New help: '\(help)' + All metrics with the same name must have identical help text. + """ + ) + } + + if let existingGauge = existingGaugeFamily.getMetric(for: labels) { + return existingGauge + } + + // Even if the metric name is identical, each label set defines a unique time series. + let gauge = Gauge(name: name, labels: labels) + let updatedFamily = existingGaugeFamily.adding(metric: gauge, for: labels) + + // Write the modified entry back to the store. + store[name] = .gauge(updatedFamily) + + return gauge + + default: + // A metric with this name exists, but it's not a Gauge. This is a programming error. + // While Prometheus wouldn't stop you, it may result in unpredictable behavior with tools like Grafana or PromQL. + fatalError( + """ + Metric type mismatch: + Could not register a Gauge with name '\(name)', + since a different metric type (\(entry.self)) was already registered with this name. + """ + ) + } + } + } + + private func _makeDurationHistogram( + name: String, + labels: [(String, String)], + buckets: [Duration], + help: String + ) -> DurationHistogram { + let name = name.ensureValidMetricName() + let labels = labels.ensureValidLabelNames() + let help = help.ensureValidHelpText() + let key = LabelsKey(labels) + + return self.box.withLockedValue { store -> DurationHistogram in + guard let entry = store[name] else { + // First time a DurationHistogram is registered with this name. This defines the buckets. + let histogram = DurationHistogram(name: name, labels: labels, buckets: buckets) + let histogramFamily = MetricFamily( + metricsByLabelSets: labels.isEmpty ? [:] : [key: histogram], + buckets: .duration(buckets), + help: help, + metricUnlabeled: labels.isEmpty ? histogram : nil + ) + store[name] = .durationHistogram(histogramFamily) + return histogram + } + + switch entry { + case .durationHistogram(let existingHistogramFamily): + + // Validate help text consistency. While Prometheus wouldn't break with duplicate and distinct + // HELP lines, the client enforces uniqueness and consistency. + if let existingHelp = existingHistogramFamily.help, existingHelp != help { + fatalError( + """ + Help text mismatch for metric '\(name)': + Existing help: '\(existingHelp)' + New help: '\(help)' + All metrics with the same name must have identical help text. + """ + ) + } + + // Validate buckets match the stored ones. + if case .duration(let storedBuckets) = existingHistogramFamily.buckets { + guard storedBuckets == buckets else { + fatalError( + """ + Bucket mismatch for DurationHistogram '\(name)': + Expected buckets: \(storedBuckets) + Provided buckets: \(buckets) + All metrics with the same name must use identical buckets. + """ + ) + } + } + + if let existingHistogram = existingHistogramFamily.getMetric(for: labels) { + return existingHistogram + } + + // Even if the metric name is identical, each label set defines a unique time series. + let histogram = DurationHistogram(name: name, labels: labels, buckets: buckets) + let updatedFamily = existingHistogramFamily.adding(metric: histogram, for: labels) + + // Write the modified entry back to the store. + store[name] = .durationHistogram(updatedFamily) + + return histogram + + default: + // A metric with this name exists, but it's not a DurationHistogram. This is a programming error. + // While Prometheus wouldn't stop you, it may result in unpredictable behavior with tools like Grafana or PromQL. + fatalError( + """ + Metric type mismatch: + Could not register a DurationHistogram with name '\(name)', + since a different metric type (\(entry.self)) was already registered with this name. + """ + ) + } + } + } + + private func _makeValueHistogram( name: String, labels: [(String, String)], buckets: [Double], @@ -798,55 +899,6 @@ public final class PrometheusCollectorRegistry: Sendable { } } - /// Creates a new ``ValueHistogram`` collector or returns the already existing one with the same name. - /// - /// When the ``PrometheusCollectorRegistry/emit(into:)`` is called, metrics from the - /// created ``ValueHistogram`` will be part of the export. - /// - /// - Parameter name: A name to identify ``ValueHistogram``'s value. - /// - Parameter labels: Labels are sets of key-value pairs that allow us to characterize and organize - /// what’s actually being measured in a Prometheus metric. - /// - Parameter buckets: Define the buckets that shall be used within the ``ValueHistogram`` - /// - Returns: A ``ValueHistogram`` that is registered with this ``PrometheusCollectorRegistry`` - public func makeValueHistogram( - name: String, - labels: [(String, String)], - buckets: [Double] - ) -> ValueHistogram { - return self.makeValueHistogram( - name: name, - labels: labels, - buckets: buckets, - help: "" - ) - } - - /// Creates a new ``ValueHistogram`` collector or returns the already existing one with the same name and labels, - /// based on the provided descriptor. - /// - /// When the ``PrometheusCollectorRegistry/emit(into:)`` is called, metrics from the - /// created ``ValueHistogram`` will be part of the export. - /// - /// - Parameter descriptor: An ``MetricNameDescriptor`` that provides the fully qualified name for the metric. - /// - Parameter labels: Labels are sets of key-value pairs that allow us to characterize and organize - /// what’s actually being measured in a Prometheus metric. - /// - Parameter buckets: Define the buckets that shall be used within the ``ValueHistogram`` - /// - Returns: A ``ValueHistogram`` that is registered with this ``PrometheusCollectorRegistry`` - public func makeValueHistogram( - descriptor: MetricNameDescriptor, - labels: [(String, String)], - buckets: [Double] - ) -> ValueHistogram { - return self.makeValueHistogram( - name: descriptor.name, - labels: labels, - buckets: buckets, - help: descriptor.helpText ?? "" - ) - } - - // MARK: - Histogram - // MARK: Destroying Metrics /// Unregisters a ``Counter`` from the ``PrometheusCollectorRegistry``. This means that the provided ``Counter``