Module: Jekyll::L10n::Instrumentation

Defined in:
lib/jekyll-l10n/instrumentation.rb

Overview

OpenTelemetry instrumentation facade for jekyll-l10n.

All tracing is configured centrally in TRACED_METHODS — no span code lives in business logic classes. To add a span: append one entry. To rename a method: update the one entry. When a method is removed from its class the stale entry raises NoMethodError in tests, signalling the entry to delete.

Requires opentelemetry-api at runtime; falls back to a no-op if absent. Users opt in to real tracing by adding opentelemetry-sdk and opentelemetry-exporter-otlp to their site Gemfile and exporting:

OTEL_SERVICE_NAME=jekyll-l10n
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318

Defined Under Namespace

Classes: NoopSpan, NoopTracer

Constant Summary collapse

TRACER_NAME =
'jekyll-l10n'
TRACED_METHODS =
[
  # ── Jekyll integration ────────────────────────────────────────────────
  ['Jekyll::L10n::Generator', :instance, :generate, 'l10n.generate',
   lambda { |span, _this, args, _result|
     span.set_attribute('l10n.site_page_count', args[0]&.pages&.size || 0)
   }],

  ['Jekyll::L10n::PostWriteProcessor', :instance, :process_localizations,
   'l10n.post_write', nil],

  # translate is the post_render hook entry point — one span per localized page
  ['Jekyll::L10n::Translator', :instance, :translate, 'l10n.translate_render',
   lambda { |span, this, _args, _result|
     span.set_attribute('l10n.locale', this.page.data['locale'].to_s)
     span.set_attribute('l10n.page_url', this.page.url.to_s)
   }],

  # ── Extraction pipeline ───────────────────────────────────────────────
  ['Jekyll::L10n::Extractor', :instance, :extract_site, 'l10n.extract_site',
   lambda { |span, _this, _args, result|
     span.set_attribute('l10n.file_count', Instrumentation.hash_val(result, :files_processed))
   }],

  # process_file is the per-page body called from the html_files loop
  ['Jekyll::L10n::Extractor', :private, :process_file, 'l10n.extract_page',
   lambda { |span, _this, args, result|
     span.set_attribute('l10n.page_path', args[0].to_s)
     span.set_attribute('l10n.strings_extracted',
                        Instrumentation.hash_val(result, :strings_extracted))
   }],

  ['Jekyll::L10n::HtmlStringExtractor', :instance, :extract, 'l10n.html_extract',
   lambda { |span, _this, args, result|
     span.set_attribute('l10n.html_size_bytes', args[0].bytesize)
     span.set_attribute('l10n.extracted_count', Instrumentation.array_size(result))
   }],

  ['Jekyll::L10n::ExtractionResultSaver', :instance, :save_results, 'l10n.po_file_write',
   lambda { |span, _this, args, result|
     span.set_attribute('l10n.page_path', args[2].to_s)
     span.set_attribute('l10n.entry_count', Instrumentation.array_size(args[1]))
     span.set_attribute('l10n.po_files_created',
                        Instrumentation.hash_val(result, :po_files_created))
   }],

  ['Jekyll::L10n::CompendiumMerger', :instance, :merge_compendia,
   'l10n.compendium_merge', nil],

  ['Jekyll::L10n::CompendiumTranslator', :instance, :translate_compendia,
   'l10n.translate_compendia',
   lambda { |span, _this, args, _result|
     config = args[0]
     span.set_attribute('l10n.locale_count', config.locales.size) if config.respond_to?(:locales)
   }],

  # ── Translation pipeline ──────────────────────────────────────────────
  ['Jekyll::L10n::PostWriteHtmlReprocessor', :instance, :reprocess_localized_pages,
   'l10n.reprocess_localized_pages', nil],

  # translate_html_file is the per-page body called from the localized_files loop
  ['Jekyll::L10n::PostWriteHtmlReprocessor', :private, :translate_html_file,
   'l10n.translate_page',
   lambda { |span, _this, args, _result|
     span.set_attribute('l10n.page_path', args[0].to_s)
     span.set_attribute('l10n.locale', args[1].to_s)
   }],

  ['Jekyll::L10n::PageTranslationLoader', :class, :load, 'l10n.translation_load',
   lambda { |span, _this, args, result|
     span.set_attribute('l10n.locale', args[1].to_s)
     span.set_attribute('l10n.page_path', args[2].to_s)
     span.set_attribute('l10n.entry_count', Instrumentation.hash_size(result))
   }],

  ['Jekyll::L10n::HtmlTranslator', :instance, :translate, 'l10n.dom_translate',
   lambda { |span, this, args, _result|
     span.set_attribute('l10n.locale', (args[2] || 'en').to_s)
     span.set_attribute('l10n.fallback_mode', this.fallback_mode.to_s)
   }],

  ['Jekyll::L10n::LibreTranslator', :private, :make_api_request,
   'l10n.libretranslate_batch',
   lambda { |span, _this, args, _result|
     span.set_attribute('l10n.locale', args[1].to_s)
     span.set_attribute('l10n.batch_size', args[0].is_a?(Array) ? args[0].size : 1)
   }],

  # ── Utilities ─────────────────────────────────────────────────────────
  ['Jekyll::L10n::HtmlParser', :class, :parse_document, 'l10n.html_parse',
   lambda { |span, _this, args, _result|
     span.set_attribute('l10n.html_size_bytes', args[0].bytesize)
   }],

  ['Jekyll::L10n::UrlTransformer', :class, :transform_document, 'l10n.url_transform',
   lambda { |span, _this, args, _result|
     span.set_attribute('l10n.locale', args[1].to_s)
     doc = args[0]
     span.set_attribute('l10n.href_count',
                        doc.respond_to?(:css) ? doc.css('a[href]').size : 0)
   }],

  ['Jekyll::L10n::ExternalLinkIconPreserver', :class, :preserve,
   'l10n.icon_preserve', nil],

  # ── PO file operations ────────────────────────────────────────────────
  ['Jekyll::L10n::PoFileReader', :instance, :parse_for_translation,
   'l10n.po_file_read',
   lambda { |span, this, _args, result|
     span.set_attribute('l10n.file_path', this.po_path.to_s)
     span.set_attribute('l10n.entry_count', Instrumentation.hash_size(result))
   }],

  ['Jekyll::L10n::PoFileMerger', :class, :merge_for_locale, 'l10n.po_merge',
   lambda { |span, _this, args, result|
     span.set_attribute('l10n.locale', args[2].to_s)
     span.set_attribute('l10n.merged_count', Instrumentation.hash_size(result))
   }]
].freeze

Class Method Summary collapse

Class Method Details

.array_size(val) ⇒ Object



46
47
48
# File 'lib/jekyll-l10n/instrumentation.rb', line 46

def self.array_size(val)
  val.is_a?(Array) ? val.size : 0
end

.enabled?Boolean

Returns true when OTel is requested via standard environment variables.

install! guards on this so the prepend wrappers are only applied when a real exporter is configured. In CI and local tests (no OTel env vars) business logic classes are untouched, keeping allow_any_instance_of stubs and other RSpec mechanics fully functional.

Returns:

  • (Boolean)


204
205
206
# File 'lib/jekyll-l10n/instrumentation.rb', line 204

def self.enabled?
  ENV.key?('OTEL_EXPORTER_OTLP_ENDPOINT') || ENV.key?('OTEL_SERVICE_NAME')
end

.hash_size(hash) ⇒ Object



42
43
44
# File 'lib/jekyll-l10n/instrumentation.rb', line 42

def self.hash_size(hash)
  hash.is_a?(Hash) ? hash.size : 0
end

.hash_val(hash, key) ⇒ Object

Helpers for common attribute patterns used inside TRACED_METHODS procs.



38
39
40
# File 'lib/jekyll-l10n/instrumentation.rb', line 38

def self.hash_val(hash, key)
  hash.is_a?(Hash) ? hash[key].to_i : 0
end

.install!Object

Installs wrappers on all classes listed in TRACED_METHODS using Module#prepend.

Called once at plugin load time (end of jekyll-l10n.rb, after all requires), but only when enabled? returns true. Use OTEL_EXPORTER_OTLP_ENDPOINT or OTEL_SERVICE_NAME to activate tracing.



213
214
215
216
217
218
219
220
221
# File 'lib/jekyll-l10n/instrumentation.rb', line 213

def self.install!
  return if @installed

  @installed = true
  setup_sdk!
  TRACED_METHODS.group_by { |e| e[0] }.each do |class_name, entries|
    prepend_wrappers(resolve_class(class_name), entries)
  end
end

.instrument(span_name, attributes: {}) {|span| ... } ⇒ Object

Wraps a block in an OTel span.

Parameters:

  • span_name (String)

    Dot-separated span name (e.g. ‘l10n.extract_page’)

  • attributes (Hash) (defaults to: {})

    Initial span attributes

Yield Parameters:

  • span (OpenTelemetry::Trace::Span, NoopSpan)

    Active span

Returns:

  • (Object)

    The return value of the block



188
189
190
# File 'lib/jekyll-l10n/instrumentation.rb', line 188

def self.instrument(span_name, attributes: {}, &block)
  tracer.in_span(span_name, attributes: attributes, &block)
end

.reset!Object

Resets the cached tracer and installation flag. Call in tests after changing OTel configuration.



193
194
195
196
# File 'lib/jekyll-l10n/instrumentation.rb', line 193

def self.reset!
  @tracer    = nil
  @installed = false
end

.resolve_class(name) ⇒ Object

Resolves a dot-separated class name to a constant; returns nil on NameError.



249
250
251
252
253
# File 'lib/jekyll-l10n/instrumentation.rb', line 249

def self.resolve_class(name)
  name.split('::').reduce(Object) { |m, c| m.const_get(c) }
rescue NameError
  nil
end

.tracerObject

Returns the active OTel tracer, or a no-op tracer if opentelemetry-api is absent.



171
172
173
174
175
176
177
178
179
180
# File 'lib/jekyll-l10n/instrumentation.rb', line 171

def self.tracer
  @tracer ||=
    if defined?(OpenTelemetry)
      # :nocov:
      OpenTelemetry.tracer_provider.tracer(TRACER_NAME, Jekyll::L10n::VERSION)
      # :nocov:
    else
      NoopTracer.new
    end
end