Commit 5ecbf14c authored by Bob Van Landuyt's avatar Bob Van Landuyt 💬
Browse files

Merge branch 'bvl-add-context' into 'master'

Add context with metadata and correlation

See merge request gitlab-org/labkit-ruby!22
parents 15809235 833a7af3
Loading
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ Gem::Specification.new do |spec|

  # Please maintain alphabetical order for dev dependencies
  spec.add_development_dependency "grpc-tools", "~> 1.19"
  spec.add_development_dependency "pry", "~> 0.12"
  spec.add_development_dependency "rack", "~> 2.0"
  spec.add_development_dependency "rake", "~> 12.3"
  spec.add_development_dependency "rspec", "~> 3.8.0"
@@ -35,4 +36,5 @@ Gem::Specification.new do |spec|
  spec.add_development_dependency "rubocop", "~> 0.65.0"
  spec.add_development_dependency "rubocop-rspec", "~> 1.22.1"
  spec.add_development_dependency "rufo", "~> 0.6"
  spec.add_development_dependency "sidekiq", "~> 5.2.7"
end
+2 −0
Original line number Diff line number Diff line
@@ -8,8 +8,10 @@ require "active_support/all"
# observability.
module Labkit
  autoload :Correlation, "labkit/correlation"
  autoload :Context, "labkit/context"
  autoload :Tracing, "labkit/tracing"
  autoload :Logging, "labkit/logging"
  autoload :Middleware, "labkit/middleware"
end

# rubocop:enable Naming/FileName

lib/labkit/context.rb

0 → 100644
+133 −0
Original line number Diff line number Diff line
# frozen_string_literal: true

module Labkit
  # A context can be used to provide structured information on what resources
  # GitLab is working on within a service.
  # The currently supported keys are defined in the `KNOWN_KEYS` constant.
  #
  # Values can be provided by passing a hash. If one of the values is a Proc
  # the proc will only be called when the value is actually needed.
  #
  # Multiple contexts can be nested, the nested context will inherit the values
  # from the closest outer one.
  # All contexts will have the same correlation id.
  #
  # Usage:
  #   Labkit::Context.with_context(user: 'username', root_namespace: -> { get_root_namespace } do |context|
  #     logger.info(context.to_h)
  #   end
  #
  class Context
    LOG_KEY = "meta"
    CORRELATION_ID_KEY = "correlation_id"
    RAW_KEYS = [CORRELATION_ID_KEY].freeze
    KNOWN_KEYS = %w[user project root_namespace].freeze

    class << self
      def with_context(attributes = {})
        context = push(attributes)

        begin
          yield(context)
        ensure
          pop(context)
        end
      end

      def push(new_attributes = {})
        new_context = current&.merge(new_attributes) || new(new_attributes)

        contexts.push(new_context)

        new_context
      end

      def pop(context)
        contexts.pop while contexts.include?(context)
      end

      def correlation_id
        contexts.last&.correlation_id
      end

      def current
        contexts.last
      end

      def log_key(key)
        key = key.to_s
        return key if RAW_KEYS.include?(key)
        return key if key.starts_with?("#{LOG_KEY}.")

        "#{LOG_KEY}.#{key}"
      end

      def known_log_keys
        @known_log_keys ||= (KNOWN_KEYS.map(&method(:log_key)) + RAW_KEYS).freeze
      end

      private

      def contexts
        Thread.current[:labkit_contexts] ||= []
      end
    end

    def initialize(values = {})
      @data = {}

      assign_attributes(values)
    end

    def merge(new_attributes)
      new_context = self.class.new(data.dup)
      new_context.assign_attributes(new_attributes)

      new_context
    end

    def to_h
      expand_data
    end

    def correlation_id
      data[CORRELATION_ID_KEY]
    end

    protected

    def assign_attributes(attributes)
      attributes = attributes.transform_keys(&method(:log_key))
      attributes = attributes.slice(*known_log_keys)

      data.merge!(attributes)

      # Remove keys that had their values set to `nil` in the new attributes
      data.keep_if { |_, value| value.present? }

      # Assign a correlation if it was missing in the first context or when
      # explicitly removed
      data[CORRELATION_ID_KEY] ||= new_id

      data
    end

    private

    delegate :log_key, :known_log_keys, to: :class

    attr_reader :data

    def expand_data
      data.transform_values do |value|
        value = value.respond_to?(:call) ? value.call : value

        value.presence
      end.compact
    end

    def new_id
      SecureRandom.hex
    end
  end
end
+6 −23
Original line number Diff line number Diff line
@@ -5,38 +5,21 @@ module Labkit
    # CorrelationId module provides access the Correlation-ID
    # of the current request
    module CorrelationId
      LOG_KEY = "correlation_id"
      LOG_KEY = Labkit::Context::CORRELATION_ID_KEY

      class << self
        def use_id(correlation_id, &_blk)
          # always generate a id if null is passed
          correlation_id ||= new_id

          ids.push(correlation_id || new_id)

          begin
            yield(current_id)
          ensure
            ids.pop
        def use_id(correlation_id)
          Labkit::Context.with_context(LOG_KEY => correlation_id) do |context|
            yield(context.correlation_id)
          end
        end

        def current_id
          ids.last
          Labkit::Context.correlation_id
        end

        def current_or_new_id
          current_id || new_id
        end

        private

        def ids
          Thread.current[:correlation_id] ||= []
        end

        def new_id
          SecureRandom.uuid
          current_id || Labkit::Context.push.correlation_id
        end
      end
    end
+9 −0
Original line number Diff line number Diff line
# frozen_string_literal: true

module Labkit
  # Adds middlewares for using in rack and sidekiq
  module Middleware
    autoload :Rack, "labkit/middleware/rack"
    autoload :Sidekiq, "labkit/middleware/sidekiq"
  end
end
Loading