Skip to content

Commit a99e0b3

Browse files
committed
Merge pull request gjtorikian#45 from mtodd/instrumented-pipeline
Instrument filters in the pipeline
2 parents 3bd6af3 + c8bfbfa commit a99e0b3

File tree

4 files changed

+145
-5
lines changed

4 files changed

+145
-5
lines changed

README.md

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,7 @@ pipeline = HTML::Pipeline.new [
5353
result = pipeline.call <<-CODE
5454
This is *great*:
5555
56-
``` ruby
57-
some_code(:first)
58-
```
56+
some_code(:first)
5957
6058
CODE
6159
result[:output].to_s
@@ -178,7 +176,7 @@ require 'uri'
178176
class RootRelativeFilter < HTML::Pipeline::Filter
179177

180178
def call
181-
doc.search("img").each do |img|
179+
doc.search("img").each do |img|
182180
next if img['src'].nil?
183181
src = img['src'].strip
184182
if src.start_with? '/'
@@ -197,6 +195,44 @@ Now this filter can be used in a pipeline:
197195
Pipeline.new [ RootRelativeFilter ], { :base_url => 'http://somehost.com' }
198196
```
199197

198+
## Instrumenting
199+
200+
To instrument each filter and a full pipeline call, set an
201+
[ActiveSupport::Notifications](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html)
202+
service object on a pipeline object. New pipeline objects will default to the
203+
`HTML::Pipeline.default_instrumentation_service` object.
204+
205+
``` ruby
206+
# the AS::Notifications-compatible service object
207+
service = ActiveSupport::Notifications
208+
209+
# instrument a specific pipeline
210+
pipeline = HTML::Pipeline.new [MarkdownFilter], context
211+
pipeline.instrumentation_service = service
212+
213+
# or instrument all new pipelines
214+
HTML::Pipeline.default_instrumentation_service = service
215+
```
216+
217+
Filters are instrumented when they are run through the pipeline. A
218+
`call_filter.html_pipeline` event is published once the filter finishes. The
219+
`payload` should include the `filter` name. Each filter will trigger its own
220+
instrumentation call.
221+
222+
``` ruby
223+
service.subscribe "call_filter.html_pipeline" do |event, start, ending, transaction_id, payload|
224+
payload[:filter] #=> "MarkdownFilter"
225+
end
226+
```
227+
228+
The full pipeline is also instrumented:
229+
230+
``` ruby
231+
service.subscribe "call_pipeline.html_pipeline" do |event, start, ending, transaction_id, payload|
232+
payload[:filters] #=> ["MarkdownFilter"]
233+
end
234+
```
235+
200236
## Development
201237

202238
To see what has changed in recent versions, see the [CHANGELOG](https://github.com/jch/html-pipeline/blob/master/CHANGELOG.md).

lib/html/pipeline.rb

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,21 @@ def self.parse(document_or_html)
6161
# Public: Returns an Array of Filter objects for this Pipeline.
6262
attr_reader :filters
6363

64+
# Public: Instrumentation service for the pipeline.
65+
# Set an ActiveSupport::Notifications compatible object to enable.
66+
attr_accessor :instrumentation_service
67+
68+
class << self
69+
# Public: Default instrumentation service for new pipeline objects.
70+
attr_accessor :default_instrumentation_service
71+
end
72+
6473
def initialize(filters, default_context = {}, result_class = nil)
6574
raise ArgumentError, "default_context cannot be nil" if default_context.nil?
6675
@filters = filters.flatten.freeze
6776
@default_context = default_context.freeze
6877
@result_class = result_class || Hash
78+
@instrumentation_service = self.class.default_instrumentation_service
6979
end
7080

7181
# Apply all filters in the pipeline to the given HTML.
@@ -84,10 +94,37 @@ def call(html, context = {}, result = nil)
8494
context = @default_context.merge(context)
8595
context = context.freeze
8696
result ||= @result_class.new
87-
result[:output] = @filters.inject(html) { |doc, filter| filter.call(doc, context, result) }
97+
instrument "call_pipeline.html_pipeline", :filters => @filters.map(&:name) do
98+
result[:output] =
99+
@filters.inject(html) do |doc, filter|
100+
perform_filter(filter, doc, context, result)
101+
end
102+
end
88103
result
89104
end
90105

106+
# Internal: Applies a specific filter to the supplied doc.
107+
#
108+
# The filter is instrumented.
109+
#
110+
# Returns the result of the filter.
111+
def perform_filter(filter, doc, context, result)
112+
instrument "call_filter.html_pipeline", :filter => filter.name do
113+
filter.call(doc, context, result)
114+
end
115+
end
116+
117+
# Internal: if the `instrumentation_service` object is set, instruments the
118+
# block, otherwise the block is ran without instrumentation.
119+
#
120+
# Returns the result of the provided block.
121+
def instrument(event, payload = nil)
122+
return yield unless instrumentation_service
123+
instrumentation_service.instrument event, payload do
124+
yield
125+
end
126+
end
127+
91128
# Like call but guarantee the value returned is a DocumentFragment.
92129
# Pipelines may return a DocumentFragment or a String. Callers that need a
93130
# DocumentFragment should use this method.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
class MockedInstrumentationService
2+
attr_reader :events
3+
def initialize(event = nil, events = [])
4+
@events = events
5+
subscribe event
6+
end
7+
def instrument(event, payload = nil)
8+
res = yield
9+
events << [event, payload, res] if @subscribe == event
10+
res
11+
end
12+
def subscribe(event)
13+
@subscribe = event
14+
end
15+
end

test/html/pipeline_test.rb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
require "test_helper"
2+
require "helpers/mocked_instrumentation_service"
3+
4+
class HTML::PipelineTest < Test::Unit::TestCase
5+
Pipeline = HTML::Pipeline
6+
class TestFilter
7+
def self.call(input, context, result)
8+
input
9+
end
10+
end
11+
12+
def setup
13+
@context = {}
14+
@result_class = Hash
15+
@pipeline = Pipeline.new [TestFilter], @context, @result_class
16+
end
17+
18+
def test_filter_instrumentation
19+
service = MockedInstrumentationService.new
20+
service.subscribe "call_filter.html_pipeline"
21+
@pipeline.instrumentation_service = service
22+
filter("hello")
23+
event, payload, res = service.events.pop
24+
assert event, "event expected"
25+
assert_equal "call_filter.html_pipeline", event
26+
assert_equal TestFilter.name, payload[:filter]
27+
end
28+
29+
def test_pipeline_instrumentation
30+
service = MockedInstrumentationService.new
31+
service.subscribe "call_pipeline.html_pipeline"
32+
@pipeline.instrumentation_service = service
33+
filter("hello")
34+
event, payload, res = service.events.pop
35+
assert event, "event expected"
36+
assert_equal "call_pipeline.html_pipeline", event
37+
assert_equal @pipeline.filters.map(&:name), payload[:filters]
38+
end
39+
40+
def test_default_instrumentation_service
41+
service = 'default'
42+
Pipeline.default_instrumentation_service = service
43+
pipeline = Pipeline.new [], @context, @result_class
44+
assert_equal service, pipeline.instrumentation_service
45+
ensure
46+
Pipeline.default_instrumentation_service = nil
47+
end
48+
49+
def filter(input)
50+
@pipeline.call(input)
51+
end
52+
end

0 commit comments

Comments
 (0)