%%%------------------------------------------------------------------- %%% Licensed to the Apache Software Foundation (ASF) under one %%% or more contributor license agreements. See the NOTICE file %%% distributed with this work for additional information %%% regarding copyright ownership. The ASF licenses this file %%% to you under the Apache License, Version 2.0 (the %%% "License"); you may not use this file except in compliance %%% with the License. You may obtain a copy of the License at %%% %%% http://www.apache.org/licenses/LICENSE-2.0 %%% %%% Unless required by applicable law or agreed to in writing, %%% software distributed under the License is distributed on an %%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY %%% KIND, either express or implied. See the License for the %%% specific language governing permissions and limitations %%% under the License. %%% %%% %%% @doc %%% The main idea behind this filter that the processing of the spans can %%% be modified runtime by changing the filter configuration. This way %%% logging, counting or sending data to trace collectors can be modified %%% on the running system based on the changing operational requirements. %%% %%% The span filter works with key value pair lists. This was the %%% easiest to implement and reasonably fast. %%% %%% Filter rules are composed by a list of `{Conditions, Actions}' tuples. %%% Processing a span means iterating through this list and when an item %%% found where all `Conditions' evaluate to true then the `Actions' in that %%% item are executed. %%% %%% `Conditions' operate on a copy of the tags of the span. It is a %%% sequence of checks against the tags (e.g. key present, key value) %%% where if any check in the sequence fails, the associated actions are %%% not executed and the next `{Conditions, Actions}' item is evaluated. %%% `Actions' can trigger e.g. counting a particular tag value combination, %%% logging, sending the span to trace collectors, modifying the working %%% (i.e. used for further conditions) tag list, modifying the span tag %%% list that is to be sent to trace collector. `Actions' can also %%% influence the further evaluation of the rules i.e. providing the %%% break action instructs the rule engine NOT to look further in the %%% list of `{Conditions, Actions}'. %%% %%% `Evaluation' of the rules happens in the process which invokes the span %%% end statement (e.g. @link otter:finish/1.) i.e. it has impact on the %%% request processing time. Therefore the actions that consume little %%% time and resources with no external interfaces (e.g. counting in ets) %%% can be done during the evaluation of the rules, but anything that has %%% external interface or dependent on environment (e.g. logging and trace %%% collecting) should be done asynchronously. %%% @end %%%------------------------------------------------------------------- -module(otter_filter). -export([ span/1 ]). -include("otter.hrl"). %%-------------------------------------------------------------------- %% @doc Takes a span as input, evaluates rules specified in %% configuration and executes any specified actions. %% @end %% -------------------------------------------------------------------- -spec span(span()) -> ok. span(#span{tags = Tags, name = Name, duration = Duration} = Span) -> Rules = otter_config:read(filter_rules, []), rules(Rules, [ {otter_span_name, Name}, {otter_span_duration, Duration}| Tags ], Span). rules([{Conditions, Actions} | Rest], Tags, Span) -> case check_conditions(Conditions, Tags) of false -> rules(Rest, Tags, Span); true -> case do_actions(Actions, Tags, Span) of break -> ok; {continue, NewTags, NewSpan} -> rules(Rest, NewTags, NewSpan) end end; rules(_, _, _) -> ok. check_conditions([Condition | Rest], Tags) -> case check(Condition, Tags) of true -> check_conditions(Rest, Tags); false -> false end; check_conditions([], _) -> true. check({negate, Condition}, Tags) -> not check(Condition, Tags); check({value, Key, Value}, Tags) -> case lists:keyfind(Key, 1, Tags) of {Key, Value} -> true; _ -> false end; check({same, Key1, Key2}, Tags) -> case lists:keyfind(Key1, 1, Tags) of {Key1, Value} -> case lists:keyfind(Key2, 1, Tags) of {Key2, Value} -> true; _ -> false end; _ -> false end; check({greater, Key, Value}, Tags) -> case lists:keyfind(Key, 1, Tags) of {Key, Value1} when Value1 > Value -> true; _ -> false end; check({less, Key, Value}, Tags) -> case lists:keyfind(Key, 1, Tags) of {Key, Value1} when Value1 < Value -> true; _ -> false end; check({between, Key, Value1, Value2}, Tags) -> case lists:keyfind(Key, 1, Tags) of {Key, Value} when Value > Value1 andalso Value < Value2 -> true; _ -> false end; check({present, Key}, Tags) -> lists:keymember(Key, 1, Tags); check(_, _) -> false. do_actions(Actions, Tags, Span) -> do_actions(Actions, Tags, Span, continue). do_actions([break | Rest], Tags, Span, _BreakOrContinue) -> do_actions(Rest, Tags, Span, break); do_actions([continue | Rest], Tags, Span, _BreakOrContinue) -> do_actions(Rest, Tags, Span, continue); do_actions([Action | Rest], Tags, Span, BreakOrContinue) -> do_actions(Rest, action(Action, Tags, Span), Span, BreakOrContinue); do_actions([], _Tags, _Span, break) -> break; do_actions([], Tags, Span, continue) -> {continue, Tags, Span}. action(send_to_zipkin, Tags, Span) -> otter_conn_zipkin:store_span(Span), Tags; action({snapshot_count, Prefix, TagNames}, Tags, Span) -> TagValues = [ case lists:keyfind(Key, 1, Tags) of {Key, Value} -> Value; _ -> undefined end || Key <- TagNames ], otter_snapshot_count:snapshot(Prefix ++ TagValues, Span), Tags; action(_, Tags, _) -> Tags.