jekyll-l10n 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 21d081d43cd7503f98fb38d64526397eaf5ff3164e0eb3d6525aa8d88290b9d7
4
- data.tar.gz: 382bb065f52fed1d762c2cd5110afc349de73a4780d564f813e8a7be4929373b
3
+ metadata.gz: a3b778612621ba8532ed88057a833cebfb615d4e7dfeb09befb9f58249896a5b
4
+ data.tar.gz: 399997d1222baaeeea243b1cc61638b169e797d72da8cc543c3949f8b63b5f4b
5
5
  SHA512:
6
- metadata.gz: cab0e3382f953d8f88acc764be019ccdb02bf382e65a4f227f99206e7e6c4370e4a6b158da960cf7a762e28d84765f75917ee0f351cfcd2b434aecd8d8bb8969
7
- data.tar.gz: f5dc58255fd89efe2014d63e9a916d9ff3ff04d20ebf67a0d863469fd082bee7d3b399586a1f32392f78b2e4f1e6cb7c26254986faacaf0b1208a6fe5ed0c3f6
6
+ metadata.gz: 122a6388d40c31898704cf3f91b96ef226d30fd9a8b706e7a68d0639bab07042d2707809ecd5076b2d84c79384824cca7964582bd4f931f4a26c95cedd4c567f
7
+ data.tar.gz: 6de61435ff7b179e62e1c7b3dbf705f6a8059a8b5b3f7158f9e2542145ddf3f391493b0a3e2dcff729c142d500658a0ce98815f514449aa27edfbcde4fe73566
@@ -0,0 +1,306 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module L10n
5
+ # OpenTelemetry instrumentation facade for jekyll-l10n.
6
+ #
7
+ # All tracing is configured centrally in TRACED_METHODS — no span code lives
8
+ # in business logic classes. To add a span: append one entry. To rename a
9
+ # method: update the one entry. When a method is removed from its class the
10
+ # stale entry raises NoMethodError in tests, signalling the entry to delete.
11
+ #
12
+ # Requires opentelemetry-api at runtime; falls back to a no-op if absent.
13
+ # Users opt in to real tracing by adding opentelemetry-sdk and
14
+ # opentelemetry-exporter-otlp to their site Gemfile and exporting:
15
+ #
16
+ # OTEL_SERVICE_NAME=jekyll-l10n
17
+ # OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
18
+ module Instrumentation
19
+ TRACER_NAME = 'jekyll-l10n'
20
+
21
+ # Central instrumentation registry.
22
+ #
23
+ # Each row: [class_name, method_type, method_name, span_name, attribute_proc]
24
+ #
25
+ # method_type:
26
+ # :instance — public instance method
27
+ # :private — private instance method (visibility is preserved on the wrapper)
28
+ # :class — class / module method (prepended on the singleton class)
29
+ #
30
+ # attribute_proc: ->(span, this, args, result) or nil
31
+ # span — OTel span (or NoopSpan); call span.set_attribute after super returns
32
+ # this — receiver object (instance or nil for class methods)
33
+ # args — positional args array as passed to the method
34
+ # result — return value of the original method
35
+ # :nocov: — attribute procs are configuration data; coverage comes from integration tests
36
+
37
+ # Helpers for common attribute patterns used inside TRACED_METHODS procs.
38
+ def self.hash_val(hash, key)
39
+ hash.is_a?(Hash) ? hash[key].to_i : 0
40
+ end
41
+
42
+ def self.hash_size(hash)
43
+ hash.is_a?(Hash) ? hash.size : 0
44
+ end
45
+
46
+ def self.array_size(val)
47
+ val.is_a?(Array) ? val.size : 0
48
+ end
49
+
50
+ TRACED_METHODS = [
51
+ # ── Jekyll integration ────────────────────────────────────────────────
52
+ ['Jekyll::L10n::Generator', :instance, :generate, 'l10n.generate',
53
+ lambda { |span, _this, args, _result|
54
+ span.set_attribute('l10n.site_page_count', args[0]&.pages&.size || 0)
55
+ }],
56
+
57
+ ['Jekyll::L10n::PostWriteProcessor', :instance, :process_localizations,
58
+ 'l10n.post_write', nil],
59
+
60
+ # translate is the post_render hook entry point — one span per localized page
61
+ ['Jekyll::L10n::Translator', :instance, :translate, 'l10n.translate_render',
62
+ lambda { |span, this, _args, _result|
63
+ span.set_attribute('l10n.locale', this.page.data['locale'].to_s)
64
+ span.set_attribute('l10n.page_url', this.page.url.to_s)
65
+ }],
66
+
67
+ # ── Extraction pipeline ───────────────────────────────────────────────
68
+ ['Jekyll::L10n::Extractor', :instance, :extract_site, 'l10n.extract_site',
69
+ lambda { |span, _this, _args, result|
70
+ span.set_attribute('l10n.file_count', Instrumentation.hash_val(result, :files_processed))
71
+ }],
72
+
73
+ # process_file is the per-page body called from the html_files loop
74
+ ['Jekyll::L10n::Extractor', :private, :process_file, 'l10n.extract_page',
75
+ lambda { |span, _this, args, result|
76
+ span.set_attribute('l10n.page_path', args[0].to_s)
77
+ span.set_attribute('l10n.strings_extracted',
78
+ Instrumentation.hash_val(result, :strings_extracted))
79
+ }],
80
+
81
+ ['Jekyll::L10n::HtmlStringExtractor', :instance, :extract, 'l10n.html_extract',
82
+ lambda { |span, _this, args, result|
83
+ span.set_attribute('l10n.html_size_bytes', args[0].bytesize)
84
+ span.set_attribute('l10n.extracted_count', Instrumentation.array_size(result))
85
+ }],
86
+
87
+ ['Jekyll::L10n::ExtractionResultSaver', :instance, :save_results, 'l10n.po_file_write',
88
+ lambda { |span, _this, args, result|
89
+ span.set_attribute('l10n.page_path', args[2].to_s)
90
+ span.set_attribute('l10n.entry_count', Instrumentation.array_size(args[1]))
91
+ span.set_attribute('l10n.po_files_created',
92
+ Instrumentation.hash_val(result, :po_files_created))
93
+ }],
94
+
95
+ ['Jekyll::L10n::CompendiumMerger', :instance, :merge_compendia,
96
+ 'l10n.compendium_merge', nil],
97
+
98
+ ['Jekyll::L10n::CompendiumTranslator', :instance, :translate_compendia,
99
+ 'l10n.translate_compendia',
100
+ lambda { |span, _this, args, _result|
101
+ config = args[0]
102
+ span.set_attribute('l10n.locale_count', config.locales.size) if config.respond_to?(:locales)
103
+ }],
104
+
105
+ # ── Translation pipeline ──────────────────────────────────────────────
106
+ ['Jekyll::L10n::PostWriteHtmlReprocessor', :instance, :reprocess_localized_pages,
107
+ 'l10n.reprocess_localized_pages', nil],
108
+
109
+ # translate_html_file is the per-page body called from the localized_files loop
110
+ ['Jekyll::L10n::PostWriteHtmlReprocessor', :private, :translate_html_file,
111
+ 'l10n.translate_page',
112
+ lambda { |span, _this, args, _result|
113
+ span.set_attribute('l10n.page_path', args[0].to_s)
114
+ span.set_attribute('l10n.locale', args[1].to_s)
115
+ }],
116
+
117
+ ['Jekyll::L10n::PageTranslationLoader', :class, :load, 'l10n.translation_load',
118
+ lambda { |span, _this, args, result|
119
+ span.set_attribute('l10n.locale', args[1].to_s)
120
+ span.set_attribute('l10n.page_path', args[2].to_s)
121
+ span.set_attribute('l10n.entry_count', Instrumentation.hash_size(result))
122
+ }],
123
+
124
+ ['Jekyll::L10n::HtmlTranslator', :instance, :translate, 'l10n.dom_translate',
125
+ lambda { |span, this, args, _result|
126
+ span.set_attribute('l10n.locale', (args[2] || 'en').to_s)
127
+ span.set_attribute('l10n.fallback_mode', this.fallback_mode.to_s)
128
+ }],
129
+
130
+ ['Jekyll::L10n::LibreTranslator', :private, :make_api_request,
131
+ 'l10n.libretranslate_batch',
132
+ lambda { |span, _this, args, _result|
133
+ span.set_attribute('l10n.locale', args[1].to_s)
134
+ span.set_attribute('l10n.batch_size', args[0].is_a?(Array) ? args[0].size : 1)
135
+ }],
136
+
137
+ # ── Utilities ─────────────────────────────────────────────────────────
138
+ ['Jekyll::L10n::HtmlParser', :class, :parse_document, 'l10n.html_parse',
139
+ lambda { |span, _this, args, _result|
140
+ span.set_attribute('l10n.html_size_bytes', args[0].bytesize)
141
+ }],
142
+
143
+ ['Jekyll::L10n::UrlTransformer', :class, :transform_document, 'l10n.url_transform',
144
+ lambda { |span, _this, args, _result|
145
+ span.set_attribute('l10n.locale', args[1].to_s)
146
+ doc = args[0]
147
+ span.set_attribute('l10n.href_count',
148
+ doc.respond_to?(:css) ? doc.css('a[href]').size : 0)
149
+ }],
150
+
151
+ ['Jekyll::L10n::ExternalLinkIconPreserver', :class, :preserve,
152
+ 'l10n.icon_preserve', nil],
153
+
154
+ # ── PO file operations ────────────────────────────────────────────────
155
+ ['Jekyll::L10n::PoFileReader', :instance, :parse_for_translation,
156
+ 'l10n.po_file_read',
157
+ lambda { |span, this, _args, result|
158
+ span.set_attribute('l10n.file_path', this.po_path.to_s)
159
+ span.set_attribute('l10n.entry_count', Instrumentation.hash_size(result))
160
+ }],
161
+
162
+ ['Jekyll::L10n::PoFileMerger', :class, :merge_for_locale, 'l10n.po_merge',
163
+ lambda { |span, _this, args, result|
164
+ span.set_attribute('l10n.locale', args[2].to_s)
165
+ span.set_attribute('l10n.merged_count', Instrumentation.hash_size(result))
166
+ }]
167
+ ].freeze
168
+ # :nocov:
169
+
170
+ # Returns the active OTel tracer, or a no-op tracer if opentelemetry-api is absent.
171
+ def self.tracer
172
+ @tracer ||=
173
+ if defined?(OpenTelemetry)
174
+ # :nocov:
175
+ OpenTelemetry.tracer_provider.tracer(TRACER_NAME, Jekyll::L10n::VERSION)
176
+ # :nocov:
177
+ else
178
+ NoopTracer.new
179
+ end
180
+ end
181
+
182
+ # Wraps a block in an OTel span.
183
+ #
184
+ # @param span_name [String] Dot-separated span name (e.g. 'l10n.extract_page')
185
+ # @param attributes [Hash] Initial span attributes
186
+ # @yieldparam span [OpenTelemetry::Trace::Span, NoopSpan] Active span
187
+ # @return [Object] The return value of the block
188
+ def self.instrument(span_name, attributes: {}, &block)
189
+ tracer.in_span(span_name, attributes: attributes, &block)
190
+ end
191
+
192
+ # Resets the cached tracer and installation flag. Call in tests after changing OTel configuration.
193
+ def self.reset!
194
+ @tracer = nil
195
+ @installed = false
196
+ end
197
+
198
+ # Returns true when OTel is requested via standard environment variables.
199
+ #
200
+ # install! guards on this so the prepend wrappers are only applied when
201
+ # a real exporter is configured. In CI and local tests (no OTel env vars)
202
+ # business logic classes are untouched, keeping allow_any_instance_of stubs
203
+ # and other RSpec mechanics fully functional.
204
+ def self.enabled?
205
+ ENV.key?('OTEL_EXPORTER_OTLP_ENDPOINT') || ENV.key?('OTEL_SERVICE_NAME')
206
+ end
207
+
208
+ # Installs wrappers on all classes listed in TRACED_METHODS using Module#prepend.
209
+ #
210
+ # Called once at plugin load time (end of jekyll-l10n.rb, after all requires),
211
+ # but only when enabled? returns true. Use OTEL_EXPORTER_OTLP_ENDPOINT or
212
+ # OTEL_SERVICE_NAME to activate tracing.
213
+ def self.install!
214
+ return if @installed
215
+
216
+ @installed = true
217
+ setup_sdk!
218
+ TRACED_METHODS.group_by { |e| e[0] }.each do |class_name, entries|
219
+ prepend_wrappers(resolve_class(class_name), entries)
220
+ end
221
+ end
222
+
223
+ # Configures the OTel SDK from standard env vars if the SDK gem is available.
224
+ #
225
+ # Called from install! so that spans are exported to the configured backend
226
+ # without requiring sites to add a separate initializer. Silently no-ops when
227
+ # opentelemetry-sdk or opentelemetry-exporter-otlp is not installed.
228
+ #
229
+ # SimpleSpanProcessor is used instead of the SDK default (BatchSpanProcessor)
230
+ # because Jekyll is a short-lived CLI process. BatchSpanProcessor exports on a
231
+ # 5-second schedule; outer spans that close last (post_write, generate) are
232
+ # silently dropped when the process exits before the next flush. Simple exports
233
+ # each span synchronously the moment it closes, guaranteeing no span loss.
234
+ def self.setup_sdk!
235
+ # :nocov:
236
+ require 'opentelemetry/sdk'
237
+ require 'opentelemetry/exporter/otlp'
238
+ exporter = OpenTelemetry::Exporter::OTLP::Exporter.new
239
+ processor = OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(exporter)
240
+ OpenTelemetry::SDK.configure do |c|
241
+ c.add_span_processor(processor)
242
+ end
243
+ rescue LoadError
244
+ nil
245
+ # :nocov:
246
+ end
247
+
248
+ # Resolves a dot-separated class name to a constant; returns nil on NameError.
249
+ def self.resolve_class(name)
250
+ name.split('::').reduce(Object) { |m, c| m.const_get(c) }
251
+ rescue NameError
252
+ nil
253
+ end
254
+
255
+ # Applies instance and class-method wrappers to klass; skips if klass is nil.
256
+ def self.prepend_wrappers(klass, entries)
257
+ return unless klass
258
+
259
+ class_entries, instance_entries = entries.partition { |e| e[1] == :class }
260
+ klass.prepend(build_wrapper_module(instance_entries)) unless instance_entries.empty?
261
+ klass.singleton_class.prepend(build_wrapper_module(class_entries)) unless class_entries.empty?
262
+ end
263
+
264
+ # Builds an anonymous module that wraps each listed method in a span.
265
+ def self.build_wrapper_module(entries)
266
+ Module.new do
267
+ entries.each do |_class_name, method_type, method_name, span_name, attr_proc|
268
+ define_method(method_name) do |*args, **kwargs, &blk|
269
+ Instrumentation.instrument(span_name) do |span|
270
+ result = super(*args, **kwargs, &blk)
271
+ attr_proc&.call(span, self, args, result)
272
+ result
273
+ rescue StandardError => e
274
+ span.record_exception(e)
275
+ raise
276
+ end
277
+ end
278
+ private method_name if method_type == :private
279
+ end
280
+ end
281
+ end
282
+
283
+ private_class_method :build_wrapper_module, :prepend_wrappers, :setup_sdk!
284
+
285
+ # No-op tracer used when opentelemetry-api is not loaded.
286
+ class NoopTracer
287
+ def in_span(_name, **_opts)
288
+ yield NoopSpan.new
289
+ end
290
+ end
291
+
292
+ # No-op span that silently accepts all attribute and event calls.
293
+ class NoopSpan
294
+ def set_attribute(*)
295
+ self
296
+ end
297
+
298
+ def record_exception(*)
299
+ self
300
+ end
301
+
302
+ def status=(*); end
303
+ end
304
+ end
305
+ end
306
+ end
@@ -36,6 +36,8 @@ module Jekyll
36
36
  MSGSTR_PATTERN = /^msgstr ['"](.*)['"] *$/.freeze unless const_defined?(:MSGSTR_PATTERN)
37
37
  NO_REFERENCE = nil unless const_defined?(:NO_REFERENCE)
38
38
 
39
+ attr_reader :po_path
40
+
39
41
  # Initialize a new PoFileReader.
40
42
  #
41
43
  # Accepts either a file path (if file exists) or inline PO content. Determines
data/lib/jekyll-l10n.rb CHANGED
@@ -170,6 +170,7 @@ require 'gettext/po'
170
170
  # @see Jekyll::L10n::UrlFilter for liquid filters
171
171
 
172
172
  require_relative 'jekyll-l10n/version'
173
+ require_relative 'jekyll-l10n/instrumentation'
173
174
  require_relative 'jekyll-l10n/constants'
174
175
  require_relative 'jekyll-l10n/errors'
175
176
  require_relative 'jekyll-l10n/po_file/reader'
@@ -271,3 +272,5 @@ Jekyll::Hooks.register :site, :post_write, priority: :high do |site|
271
272
  end
272
273
 
273
274
  Liquid::Template.register_filter(Jekyll::L10n::UrlFilter)
275
+
276
+ Jekyll::L10n::Instrumentation.install! if Jekyll::L10n::Instrumentation.enabled?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-l10n
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alain Reguera Delgado
@@ -69,6 +69,20 @@ dependencies:
69
69
  - - "<"
70
70
  - !ruby/object:Gem::Version
71
71
  version: '2.0'
72
+ - !ruby/object:Gem::Dependency
73
+ name: opentelemetry-api
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - "~>"
77
+ - !ruby/object:Gem::Version
78
+ version: '1.4'
79
+ type: :runtime
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - "~>"
84
+ - !ruby/object:Gem::Version
85
+ version: '1.4'
72
86
  - !ruby/object:Gem::Dependency
73
87
  name: bundler
74
88
  requirement: !ruby/object:Gem::Requirement
@@ -230,6 +244,7 @@ files:
230
244
  - lib/jekyll-l10n/extraction/html_string_extractor.rb
231
245
  - lib/jekyll-l10n/extraction/logger.rb
232
246
  - lib/jekyll-l10n/extraction/result_saver.rb
247
+ - lib/jekyll-l10n/instrumentation.rb
233
248
  - lib/jekyll-l10n/jekyll/generator.rb
234
249
  - lib/jekyll-l10n/jekyll/localized_page.rb
235
250
  - lib/jekyll-l10n/jekyll/post_write_html_reprocessor.rb
@@ -289,7 +304,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
289
304
  - !ruby/object:Gem::Version
290
305
  version: '0'
291
306
  requirements: []
292
- rubygems_version: 3.6.9
307
+ rubygems_version: 4.0.10
293
308
  specification_version: 4
294
309
  summary: Jekyll plugin for jekyll site localization.
295
310
  test_files: []