Verified Commit d083744a authored by Bob Van Landuyt's avatar Bob Van Landuyt 💬
Browse files

feat: add SLI module for Service Level Indicators

Move the `Gitlab::Metrics::Sli` module from GitLab-rails into
labkit-ruby to enable reuse across multiple GitLab services. The
module provides `Apdex` and `ErrorRate` SLI types for observability.

This also renames the module to `ApplicationSli` to use consistent naming
as we do in the docs.

The implementation uses `Labkit::Metrics::Client` for Prometheus
counters while maintaining the 'gitlab_sli' counter prefix for
consistency.

This is an exact copy of the class and tests from GitLab-rails

For #52
parent 7f1d5c81
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@ module Labkit
  autoload :Context, "labkit/context"
  autoload :Correlation, "labkit/correlation"
  autoload :CoveredExperience, "labkit/user_experience_sli" # Backward compatibility alias
  autoload :ApplicationSli, "labkit/application_sli"
  autoload :UserExperienceSli, "labkit/user_experience_sli"
  autoload :FIPS, "labkit/fips"
  autoload :Tracing, "labkit/tracing"
+76 −0
Original line number Diff line number Diff line
# frozen_string_literal: true

module Labkit
  module ApplicationSli
    COUNTER_PREFIX = 'gitlab_sli'

    module ClassMethods
      INITIALIZATION_MUTEX = Mutex.new

      def [](name)
        known_slis[name] || initialize_sli(name, [])
      end

      def initialize_sli(name, possible_label_combinations)
        INITIALIZATION_MUTEX.synchronize do
          next known_slis[name] if initialized?(name)

          sli = new(name)
          sli.initialize_counters(possible_label_combinations)
          known_slis[name] = sli
        end
      end

      def initialized?(name)
        known_slis.key?(name) && known_slis[name].initialized?
      end

      private

      def known_slis
        @known_slis ||= {}
      end
    end

    def self.included(mod)
      mod.extend(ClassMethods)
    end

    attr_reader :name

    def initialize(name)
      @name = name
      @initialized_with_combinations = false
    end

    def initialize_counters(possible_label_combinations)
      @initialized_with_combinations = possible_label_combinations.any?
      possible_label_combinations.each do |label_combination|
        total_counter.get(label_combination)
        numerator_counter.get(label_combination)
      end
    end

    def increment(labels:, increment_numerator:)
      total_counter.increment(labels)
      numerator_counter.increment(labels) if increment_numerator
    end

    def initialized?
      @initialized_with_combinations
    end

    private

    def total_counter
      prometheus.counter(counter_name('total'), "Total number of measurements for #{name}")
    end

    def prometheus
      Labkit::Metrics::Client
    end

    autoload :Apdex, "labkit/application_sli/apdex"
    autoload :ErrorRate, "labkit/application_sli/error_rate"
  end
end
+69 −0
Original line number Diff line number Diff line
# Application SLIs

This module provides [Application Service Level Indicators(SLIs)](https://docs.gitlab.com/development/application_slis/)
for monitoring and observability. It allows defining SLIs directly in Ruby code, keeping the definition of operations and their success close to the implementation.

Two SLI types are available:

- **`Labkit::ApplicationSli::Apdex`** - Measures the performance of successful operations using a success rate.
- **`Labkit::ApplicationSli::ErrorRate`** - Measures the rate of unsuccessful operations using an error rate.

## Defining a new SLI

When you define an SLI, two [Prometheus counters](https://prometheus.io/docs/concepts/metric_types/#counter) are emitted. Both contain a total operation count, and a numerator counter for the success or error rate.

`Labkit::ApplicationSli::Apdex` defines:

- `gitlab_sli_<name>_apdex_total` - total number of measurements
- `gitlab_sli_<name>_apdex_success_total` - number of successful measurements

`Labkit::ApplicationSli::ErrorRate` defines:

- `gitlab_sli_<name>_total` - total number of measurements
- `gitlab_sli_<name>_error_total` - number of error measurements

## Initializing an SLI

Before the first Prometheus scrape, initialize the SLI with all possible label combinations to [avoid missing metrics](https://prometheus.io/docs/practices/instrumentation/#avoid-missing-metrics):

```ruby
Labkit::ApplicationSli::Apdex.initialize_sli(:received_email, [
  {
    feature_category: :team_planning,
    email_type: :create_issue
  },
  {
    feature_category: :service_desk,
    email_type: :service_desk
  }
])
```

## Tracking operations

Increment the SLI counters using the `#increment` method with the appropriate labels.

For `Apdex`, pass `success:` to indicate whether the operation met the performance target:

```ruby
Labkit::ApplicationSli::Apdex[:received_email].increment(
  labels: {
    feature_category: :service_desk,
    email_type: :service_desk
  },
  success: issue_created?
)
```

For `ErrorRate`, pass `error:` to indicate whether the operation failed:

```ruby
Labkit::ApplicationSli::ErrorRate[:merge].increment(
  labels: {
    merge_type: :fast_forward
  },
  error: !merge_success?
)
```

When `success:` (or `error:`) is truthy, both the total and numerator counters are incremented. When falsy, only the total counter is incremented.
+23 −0
Original line number Diff line number Diff line
# frozen_string_literal: true

module Labkit
  module ApplicationSli
    class Apdex
      include Labkit::ApplicationSli

      def increment(labels:, success:)
        super(labels: labels, increment_numerator: success)
      end

      private

      def counter_name(suffix)
        :"#{COUNTER_PREFIX}_#{name}_apdex_#{suffix}"
      end

      def numerator_counter
        prometheus.counter(counter_name('success_total'), "Number of successful measurements for #{name}")
      end
    end
  end
end
+23 −0
Original line number Diff line number Diff line
# frozen_string_literal: true

module Labkit
  module ApplicationSli
    class ErrorRate
      include Labkit::ApplicationSli

      def increment(labels:, error:)
        super(labels: labels, increment_numerator: error)
      end

      private

      def counter_name(suffix)
        :"#{COUNTER_PREFIX}_#{name}_#{suffix}"
      end

      def numerator_counter
        prometheus.counter(counter_name('error_total'), "Number of error measurements for #{name}")
      end
    end
  end
end
Loading