Last active
July 2, 2024 13:21
-
-
Save jdelStrother/c37082b04ee27622dd3715484177e0b5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
class BeSemanticLog | |
include RSpec::Matchers::Composable | |
def initialize(expected) | |
@expected = expected.is_a?(String) ? { message: expected } : expected | |
end | |
def matches?(event) | |
matches_event?(event, **@expected) | |
end | |
def matches_event?(event, level: nil, name: nil, message: nil, message_including: nil, | |
payload: nil, payload_including: nil, including: nil, | |
thread_name: nil, tags: nil, named_tags: nil, context: nil, | |
metric: nil, metric_amount: nil, dimensions: nil) | |
if !event | |
@failure = :no_event | |
return false | |
end | |
if message && (event.message != message) | |
@failure = :bad_message | |
return false | |
end | |
if message_including && !event.message&.include?(message_including) | |
@failure = :bad_message | |
return false | |
end | |
if name && (event.name != name) | |
@failure = :bad_name | |
return false | |
end | |
if name && (event.level != level) | |
@failure = :bad_level | |
return false | |
end | |
if payload_including && event.payload | |
payload_including.each_pair do |key, expected_value| | |
value = event.payload[key] | |
if value != expected_value | |
@failure = :bad_payload | |
return false | |
end | |
end | |
elsif payload && event.payload != payload | |
@failure = :bad_payload | |
return false | |
end | |
# this is pretty handwavey - I wanted a way to make sure we weren't exposing personal data in either the message or payload. | |
if including | |
all_content = [event.message, event.payload].compact.to_json | |
if !all_content.match?(including) | |
@failure = :bad_payload_or_message | |
return false | |
end | |
end | |
if thread_name && event.thread_name != thread_name | |
@failure = :bad_thread | |
return false | |
end | |
if named_tags && event.named_tags != named_tags | |
@failure = :bad_named_tags | |
return false | |
end | |
if tags && event.tags != tags | |
@failure = :bad_tags | |
return false | |
end | |
if context && event.context != context | |
@failure = :bad_context | |
return false | |
end | |
if metric && event.metric != metric | |
@failure = :bad_metric | |
return false | |
end | |
if metric_amount && event.metric_amount != metric_amount | |
@failure = :bad_metric_amount | |
return false | |
end | |
if dimensions && event.dimensions != dimensions | |
@failure = :bad_dimensions | |
return false | |
end | |
true | |
end | |
def description = "be a semantic log event matching #{@expected}" | |
def failure_message | |
"expected #{description}, but had #{@failure}" | |
end | |
def failure_message_when_negated | |
"expected not to #{description}" | |
end | |
end | |
# expect { | |
# User.logger.info("hello") | |
# }.to log_event(message: "hello", on: User) | |
# | |
# logs = capture_logs { | |
# post "/login", params: { username: "bob" } | |
# } | |
# expect(logs[0]).to be_semantic_log(message: "Started") | |
# expect(logs).to include(a_semantic_log(payload_including: { params: { "username" => "bob" } }) | |
# | |
RSpec::Matchers.define :log_event do |expected| | |
supports_block_expectations | |
match do |block| | |
on_class = expected.delete(:on) if expected.is_a?(Hash) | |
@actual = capture_logs(on_class) do | |
block.call | |
end | |
@actual.any? { BeSemanticLog.new(expected).matches?(_1) } | |
end | |
failure_message do | |
"Expected logs to match '#{expected.inspect}'. Received:\n #{@actual.map { _1.to_h(nil, nil, nil).inspect }.join("\n")}" | |
end | |
end | |
module SemanticLogMatchers | |
def capture_logs(on_class = nil) | |
logger = SemanticLogger::Test::CaptureLogEvents.new | |
if on_class | |
allow(on_class).to receive(:logger).and_return(logger) | |
else | |
allow(SemanticLogger::Logger).to receive(:processor).and_return(logger) | |
end | |
yield | |
logger.events | |
ensure | |
# Is this the best way of unstubbing? | |
if on_class | |
allow(on_class).to receive(:logger).and_call_original | |
else | |
allow(SemanticLogger::Logger).to receive(:processor).and_call_original | |
end | |
end | |
def a_semantic_log(...) | |
BeSemanticLog.new(...) | |
end | |
alias be_semantic_log a_semantic_log | |
end | |
RSpec.configure { _1.include(SemanticLogMatchers) } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment