Commit a99e0b3a authored by Jerry Cheung's avatar Jerry Cheung

Merge pull request #45 from mtodd/instrumented-pipeline

Instrument filters in the pipeline
parents 3bd6af3b c8bfbfaa
......@@ -53,9 +53,7 @@ pipeline = HTML::Pipeline.new [
result = pipeline.call <<-CODE
This is *great*:
``` ruby
some_code(:first)
```
some_code(:first)
CODE
result[:output].to_s
......@@ -178,7 +176,7 @@ require 'uri'
class RootRelativeFilter < HTML::Pipeline::Filter
def call
doc.search("img").each do |img|
doc.search("img").each do |img|
next if img['src'].nil?
src = img['src'].strip
if src.start_with? '/'
......@@ -197,6 +195,44 @@ Now this filter can be used in a pipeline:
Pipeline.new [ RootRelativeFilter ], { :base_url => 'http://somehost.com' }
```
## Instrumenting
To instrument each filter and a full pipeline call, set an
[ActiveSupport::Notifications](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html)
service object on a pipeline object. New pipeline objects will default to the
`HTML::Pipeline.default_instrumentation_service` object.
``` ruby
# the AS::Notifications-compatible service object
service = ActiveSupport::Notifications
# instrument a specific pipeline
pipeline = HTML::Pipeline.new [MarkdownFilter], context
pipeline.instrumentation_service = service
# or instrument all new pipelines
HTML::Pipeline.default_instrumentation_service = service
```
Filters are instrumented when they are run through the pipeline. A
`call_filter.html_pipeline` event is published once the filter finishes. The
`payload` should include the `filter` name. Each filter will trigger its own
instrumentation call.
``` ruby
service.subscribe "call_filter.html_pipeline" do |event, start, ending, transaction_id, payload|
payload[:filter] #=> "MarkdownFilter"
end
```
The full pipeline is also instrumented:
``` ruby
service.subscribe "call_pipeline.html_pipeline" do |event, start, ending, transaction_id, payload|
payload[:filters] #=> ["MarkdownFilter"]
end
```
## Development
To see what has changed in recent versions, see the [CHANGELOG](https://github.com/jch/html-pipeline/blob/master/CHANGELOG.md).
......
......@@ -61,11 +61,21 @@ module HTML
# Public: Returns an Array of Filter objects for this Pipeline.
attr_reader :filters
# Public: Instrumentation service for the pipeline.
# Set an ActiveSupport::Notifications compatible object to enable.
attr_accessor :instrumentation_service
class << self
# Public: Default instrumentation service for new pipeline objects.
attr_accessor :default_instrumentation_service
end
def initialize(filters, default_context = {}, result_class = nil)
raise ArgumentError, "default_context cannot be nil" if default_context.nil?
@filters = filters.flatten.freeze
@default_context = default_context.freeze
@result_class = result_class || Hash
@instrumentation_service = self.class.default_instrumentation_service
end
# Apply all filters in the pipeline to the given HTML.
......@@ -84,10 +94,37 @@ module HTML
context = @default_context.merge(context)
context = context.freeze
result ||= @result_class.new
result[:output] = @filters.inject(html) { |doc, filter| filter.call(doc, context, result) }
instrument "call_pipeline.html_pipeline", :filters => @filters.map(&:name) do
result[:output] =
@filters.inject(html) do |doc, filter|
perform_filter(filter, doc, context, result)
end
end
result
end
# Internal: Applies a specific filter to the supplied doc.
#
# The filter is instrumented.
#
# Returns the result of the filter.
def perform_filter(filter, doc, context, result)
instrument "call_filter.html_pipeline", :filter => filter.name do
filter.call(doc, context, result)
end
end
# Internal: if the `instrumentation_service` object is set, instruments the
# block, otherwise the block is ran without instrumentation.
#
# Returns the result of the provided block.
def instrument(event, payload = nil)
return yield unless instrumentation_service
instrumentation_service.instrument event, payload do
yield
end
end
# Like call but guarantee the value returned is a DocumentFragment.
# Pipelines may return a DocumentFragment or a String. Callers that need a
# DocumentFragment should use this method.
......
class MockedInstrumentationService
attr_reader :events
def initialize(event = nil, events = [])
@events = events
subscribe event
end
def instrument(event, payload = nil)
res = yield
events << [event, payload, res] if @subscribe == event
res
end
def subscribe(event)
@subscribe = event
end
end
require "test_helper"
require "helpers/mocked_instrumentation_service"
class HTML::PipelineTest < Test::Unit::TestCase
Pipeline = HTML::Pipeline
class TestFilter
def self.call(input, context, result)
input
end
end
def setup
@context = {}
@result_class = Hash
@pipeline = Pipeline.new [TestFilter], @context, @result_class
end
def test_filter_instrumentation
service = MockedInstrumentationService.new
service.subscribe "call_filter.html_pipeline"
@pipeline.instrumentation_service = service
filter("hello")
event, payload, res = service.events.pop
assert event, "event expected"
assert_equal "call_filter.html_pipeline", event
assert_equal TestFilter.name, payload[:filter]
end
def test_pipeline_instrumentation
service = MockedInstrumentationService.new
service.subscribe "call_pipeline.html_pipeline"
@pipeline.instrumentation_service = service
filter("hello")
event, payload, res = service.events.pop
assert event, "event expected"
assert_equal "call_pipeline.html_pipeline", event
assert_equal @pipeline.filters.map(&:name), payload[:filters]
end
def test_default_instrumentation_service
service = 'default'
Pipeline.default_instrumentation_service = service
pipeline = Pipeline.new [], @context, @result_class
assert_equal service, pipeline.instrumentation_service
ensure
Pipeline.default_instrumentation_service = nil
end
def filter(input)
@pipeline.call(input)
end
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment