diff options
author | ILYA Khlopotov <iilyak@apache.org> | 2019-11-12 16:20:47 +0000 |
---|---|---|
committer | Paul J. Davis <paul.joseph.davis@gmail.com> | 2019-11-20 16:58:56 -0600 |
commit | e93d1b45064ee76da9603c49bd43552fa12214fe (patch) | |
tree | b0a264ea9165f775e9e759294f9f6ed65eed5ff4 | |
parent | c9b8e25eac479fdcbf275ac39323fc355715620d (diff) | |
download | couchdb-e93d1b45064ee76da9603c49bd43552fa12214fe.tar.gz |
Add ctrace application
-rw-r--r-- | .gitignore | 5 | ||||
-rw-r--r-- | rebar.config.script | 5 | ||||
-rw-r--r-- | rel/overlay/etc/default.ini | 50 | ||||
-rw-r--r-- | src/couch/src/couch_util.erl | 4 | ||||
-rw-r--r-- | src/ctrace/README.md | 291 | ||||
-rw-r--r-- | src/ctrace/rebar.config | 14 | ||||
-rw-r--r-- | src/ctrace/src/ctrace.app.src | 27 | ||||
-rw-r--r-- | src/ctrace/src/ctrace.erl | 361 | ||||
-rw-r--r-- | src/ctrace/src/ctrace.hrl | 15 | ||||
-rw-r--r-- | src/ctrace/src/ctrace_app.erl | 26 | ||||
-rw-r--r-- | src/ctrace/src/ctrace_config.erl | 133 | ||||
-rw-r--r-- | src/ctrace/src/ctrace_dsl.erl | 106 | ||||
-rw-r--r-- | src/ctrace/src/ctrace_sup.erl | 41 | ||||
-rw-r--r-- | src/ctrace/test/ctrace_config_test.erl | 153 | ||||
-rw-r--r-- | src/ctrace/test/ctrace_dsl_test.erl | 123 | ||||
-rw-r--r-- | src/ctrace/test/ctrace_test.erl | 412 |
16 files changed, 1757 insertions, 9 deletions
diff --git a/.gitignore b/.gitignore index 3c8bf0db8..2de464cbd 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .venv .DS_Store .rebar/ +.rebar3/ .erlfdb/ .eunit/ log @@ -54,16 +55,20 @@ src/hyper/ src/ibrowse/ src/ioq/ src/hqueue/ +src/jaeger_passage/ src/jiffy/ src/ken/ src/khash/ +src/local/ src/meck/ src/mochiweb/ src/oauth/ +src/passage/ src/proper/ src/rebar/ src/smoosh/ src/snappy/ +src/thrift_protocol/ src/triq/ tmp/ diff --git a/rebar.config.script b/rebar.config.script index 8ef1abcc8..178cca8dd 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -87,6 +87,7 @@ SubDirs = [ "src/couch_peruser", "src/couch_tests", "src/couch_views", + "src/ctrace", "src/ddoc_cache", "src/dreyfus", "src/fabric", @@ -119,9 +120,13 @@ DepDescs = [ {folsom, "folsom", {tag, "CouchDB-0.8.3"}}, {hyper, "hyper", {tag, "CouchDB-2.2.0-4"}}, {ibrowse, "ibrowse", {tag, "CouchDB-4.0.1-1"}}, +{jaeger_passage, "jaeger-passage", {tag, "CouchDB-0.1.13-1"}}, {jiffy, "jiffy", {tag, "CouchDB-0.14.11-2"}}, +{local, "local", {tag, "0.2.1"}}, {mochiweb, "mochiweb", {tag, "v2.19.0"}}, {meck, "meck", {tag, "0.8.8"}}, +{passage, "passage", {tag, "0.2.6"}}, +{thrift_protocol, "thrift-protocol", {tag, "0.1.3"}}, %% TMP - Until this is moved to a proper Apache repo {erlfdb, "erlfdb", {branch, "master"}} diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini index 79555f3c0..63cb443fc 100644 --- a/rel/overlay/etc/default.ini +++ b/rel/overlay/etc/default.ini @@ -164,8 +164,8 @@ enable_xframe_options = false ; CouchDB can optionally enforce a maximum uri length; ; max_uri_length = 8000 ; changes_timeout = 60000 -; config_whitelist = -; max_uri_length = +; config_whitelist = +; max_uri_length = ; rewrite_limit = 100 ; x_forwarded_host = X-Forwarded-Host ; x_forwarded_proto = X-Forwarded-Proto @@ -174,7 +174,7 @@ enable_xframe_options = false max_http_request_size = 4294967296 ; 4GB ; [httpd_design_handlers] -; _view = +; _view = ; [ioq] ; concurrency = 10 @@ -188,7 +188,7 @@ port = 6984 ; [chttpd_auth_cache] ; max_lifetime = 600000 -; max_objects = +; max_objects = ; max_size = 104857600 ; [mem3] @@ -199,7 +199,7 @@ port = 6984 ; [fabric] ; all_docs_concurrency = 10 -; changes_duration = +; changes_duration = ; shard_timeout_factor = 2 ; uuid_prefix_len = 7 ; request_timeout = 60000 @@ -242,7 +242,7 @@ iterations = 10 ; iterations for password hashing ; proxy_use_secret = false ; comma-separated list of public fields, 404 if empty ; public_fields = -; secret = +; secret = ; users_db_public = false ; cookie_domain = example.com @@ -330,7 +330,7 @@ javascript = couch_js couch_mrview = true [feature_flags] -; This enables any database to be created as a partitioned databases (except system db's). +; This enables any database to be created as a partitioned databases (except system db's). ; Setting this to false will stop the creation of paritioned databases. ; paritioned||allowed* = true will scope the creation of partitioned databases ; to databases with 'allowed' prefix. @@ -530,7 +530,7 @@ min_priority = 2.0 ; The default number of results returned from a search on a partition ; of a database. ; limit_partitions = 2000 - + ; The maximum number of results that can be returned from a global ; search query (or any search query on a database without user-defined ; partitions). Attempts to set ?limit=N higher than this value will @@ -564,3 +564,37 @@ min_priority = 2.0 ; ; Jitter applied when checking for new job types. ;type_check_max_jitter_msec = 5000 + +[tracing] +; +; Configuration settings for the `ctrace` OpenTracing +; API. +; +; enabled = false ; true | false +; thrift_format = compact ; compact | binary +; agent_host = 127.0.0.1 +; agent_port = 6831 +; app_name = couchdb ; value to use for the `location.application` tag + +[tracing.filters] +; +; Configure tracing for each individual operation. Keys should be set as +; operation names (i.e., `database-info.read` or `view.build`). Values +; are essentially an anonymous function that accepts a single argument +; that is the tags provided to the root span. These definitions +; should not include a function name or a trailing `.`. Return values +; must be one of `true`, `false`, or `float()`. A boolean return +; indicates whether or not to include the trace while a `float()` +; value between 0 and 1 gives the probability that the trace should +; be included or not. I.e., if the value is `0.9` then 90% of the +; traces will be logged. See the `src/ctrace/README.md` for a +; thorough description of the filter DSL. +; +; database-info.read = (#{'http.method' := Method}) when Method == 'GET' -> true +; view.build = (#{'view.name' := Name}) when Name == "foo" -> 0.25 +; +; The key `all` is checked for any trace that does not have a +; corresponding operation name key configured. Thus, users can easily +; log every generated trace by including the following: +; +; all = (#{}) -> true diff --git a/src/couch/src/couch_util.erl b/src/couch/src/couch_util.erl index 7c64459f7..260aac6f4 100644 --- a/src/couch/src/couch_util.erl +++ b/src/couch/src/couch_util.erl @@ -54,7 +54,9 @@ <<"^native_query_servers$">>, <<"^os_daemons$">>, <<"^query_servers$">>, - <<"^feature_flags$">> + <<"^feature_flags$">>, + <<"^tracing\..*$">>, + <<"^tracing$">> ]). diff --git a/src/ctrace/README.md b/src/ctrace/README.md new file mode 100644 index 000000000..6e40b434c --- /dev/null +++ b/src/ctrace/README.md @@ -0,0 +1,291 @@ +Overview +======== + +This application provides an interface to opentracing compatible +tracing systems. + +Open Tracing +------------ + +[//]: # (taken from https://github.com/opentracing/specification/blob/master/specification.md) +Traces in OpenTracing are defined implicitly by their Spans. +In particular, a Trace can be thought of as a directed acyclic +graph (DAG) of Spans, where the edges between Spans are called +References. + +Each Span encapsulates the following state: + +- An operation name +- A start timestamp +- A finish timestamp +- A set of zero or more key:value Span Tags. +- A set of zero or more Span Logs, each of which is + itself a key:value map paired with a timestamp. +- A SpanContext +- References to zero or more causally-related Spans + +Every trace is identified by unique trace_id. Every trace includes zero +or more tracing spans which are identified by a span id. + +Jaeger +------ + +Jaeger is a distributed tracing system released as open source by +Uber Technologies. It is one of implementations of open tracing specification. +Jaeger supports Trace detail view where a single trace is represented as +a tree of tracing span with detailed timing information about every span. +In order to make this feature work all tracing spans should form a lineage +from the same root span. + + +Implementation +============== + +Every operation has unique identifier. Example identifiers are: + +- all-dbs.read +- database.delete +- replication.trigger +- view.compaction + +Tracing begins with a root span that can be filtered based on +a set of configurable rules. When the root trace is created these +rules are applied to see if the trace should be generated and logged. +If a trace is disabled due to filtering then no trace data is generated. + + +Code instrumentation +-------------------- + +The span lifecycle is controled by + +- `ctrace:start_span` +- `ctrace:finish_span` +- `ctrace:with_span` + +The instrumentation can add tags and logs to a span. + +Example of instrumentation: + +``` +ctrace:with_span('database.read', #{'db.name' => <<>>}, fun() -> + ctrace:tag(#{ + peer => Peer, + 'http.method' => Method, + nonce => Nonce, + 'http.url' => Path, + 'span.kind' => <<"server">>, + component => <<"couchdb.chttpd">> + }), + ctrace:log(#{ + field0 => "value0" + }) + + handle_request(HttpReq) +end), +``` + +As you can see the `ctrace:with_span/3` function receives a function which +wraps the operation we wanted to trace: + +- `ctrace:tag/1` to add new tags to the span +- `ctrace:log/1` add log event to the span + +There are some informative functions as well: + +- `ctrace:refs/0` - returns all other spans we have references from the current +- `ctrace:operation_name/0` - returns operation name for the current span +- `ctrace:trace_id/0` - returns trace id for the current span +- `ctrace:span_id/0` - returns span id for the current span + +Instrumentation guide +--------------------- + +- Start root span at system boundaries + - httpd + - internal trigger (replication or compaction jobs) +- Start new child span when you cross layer boundaries +- Start new child span when you cross node bounadary +- Extend `<app>_httpd_handlers:handler_info/1` as needed to + have operation ids. (We as community might need to work on + naming conventions) +- Use [span conventions](https://github.com/apache/couchdb-documentation/blob/master/rfcs/011-opentracing.md#conventions) https://github.com/opentracing/specification/blob/master/semantic_conventions.md +- When in doubt consult open tracing spec + - [spec overview](https://github.com/opentracing/specification/blob/master/specification.md) + - [conventions](https://github.com/opentracing/specification/blob/master/semantic_conventions.md#standard-span-tags-and-log-fields) + +Configuration +------------- + +Traces are configured using standard CouchDB ini file based configuration. +There is a global toggle `[tracing] enabled = true | false` that switches +tracing on or off completely. The `[tracing]` section also includes +configuration for where to send trace data. + +An example `[tracing]` section + +```ini +[tracing] + +enabled = true +thrift_format = compact ; compact | binary +agent_host = 127.0.0.1 +agent_port = 6831 +app_name = couchdb ; Value to use for the `location.application` tag +``` + +In the `[tracing.filters]` section we can define a set of rules for +whether to include a trace. Keys are the operation name of the root +span and values are a simple DSL for whether to include the given +span based on its tags. See below for a more thorough description +of the DSL. The `all` key is special and is used when no other +filter matches a given operation. If the `all` key is not present +then ctrace behaves as if it were defined as `(#{}) -> false`. I.e., +any trace that doesn't have a configuration entry is not generated +and logged. + +```ini +[tracing.filters] +; all = (#{}) -> true +; database-info.read = (#{'http.method' := Method}) when Method == 'GET' -> true +; view.build = (#{'view.name' := Name}) when Name == "foo" -> 0.25 +``` + +Filter DSL Description +--- + +``` +<operation_name> = ( #{<[arguments]>} ) when <[conditions]> -> <[actions]> +``` + +Where: + - operation_name is the name of the root span + - arguments is comma separated pairs of + `<tag_or_field_name> := <variable_name>` + - actions is a list which contains + - `report` + - conditions + - `<[condition]>` + - `| <[condition]> <[operator]> <[condition]>` + - condition: + - `<variable_name> <[operator]> <value>` + `| <[guard_function]>(<[variable_name]>)` + - `variable_name` - lowercase name without special characters + - guard_function: one of + - `is_atom` + - `is_float` + - `is_integer` + - `is_list` + - `is_number` + - `is_pid` + - `is_port` + - `is_reference` + - `is_tuple` + - `is_map` + - `is_binary` + - `is_function` + - `element` - `element(n, tuple)` + - `abs` + - `hd` - return head of the list + - `length` + - `map_get` + - `map_size` + - `round` + - `node` + - `size` - returns size of the tuple + - `bit_size` - returns number of bits in binary + - `byte_size` - returns number of bytes in binary + - `tl` - return tail of a list + - `trunc` + - `self` + - operator: one of + - `not` + - `and` - evaluates both expressions + - `andalso` - evaluates second only when first is true + - `or` - evaluates both expressions + - `orelse` - evaluates second only when first is false + - `xor` + - `+` + - `-` + - `*` + - `div` + - `rem` + - `band` - bitwise AND + - `bor` - bitwise OR + - `bxor` - bitwise XOR + - `bnot` - bitwise NOT + - `bsl` - arithmetic bitshift left + - `bsr` - bitshift right + - `>` + - `>=` + - `<` + - `=<` + - `=:=` + - `==` + - `=/=` + - `/=` - not equal + + +b3 propagation +-------------- + +In order to correlate spans across multiple systems the information +about parent span can be passed via headers. Currently the chttpd +application is responsible for extracting and parsing the header. +The ctrace application provides following facilities to enable this +use case: + +- `{root, RootSpan}` option for `ctrace:start_span/2` +- `ctrace:external_span/3` to convert references to a root span + +The span references could be set either via `b3` header of via +individual headers. In case when individual headers are used the +following set of headers is supported: + +- X-B3-TraceId (32 lower-hex characters) +- X-B3-SpanId (16 lower-hex characters) + (has no effect if X-B3-TraceId is not set) +- X-B3-ParentSpanId (16 lower-hex characters) + (has no effect if X-B3-TraceId is not set) + +Alternatively a single `b3` header could be used. It has to be +in the following format: + +b3={TraceId}-{SpanId}-{SamplingState}-{ParentSpanId} + +Where SamplingState is either `0` or `1`. However we ignore the value. + +Note: We only support 128 bit TraceId's. + +Developing +========== + +Here we provide a list frequently used commands +useful while working on this application. + + +1. Run all tests +``` +make setup-eunit +make && ERL_LIBS=`pwd`/src BUILDDIR=`pwd` mix test --trace src/chttpd/test/exunit/ src/ctrace/test/exunit/ +``` + +2. Run tests selectively +``` +make && ERL_LIBS=`pwd`/src BUILDDIR=`pwd` mix test --trace src/chttpd/test/exunit/ctrace_context_test.exs:59 +``` + +3. Re-run only failed tests +``` +make && ERL_LIBS=`pwd`/src BUILDDIR=`pwd` mix test --failed --trace src/chttpd/test/exunit/ src/ctrace/test/exunit/ +``` + +4. Running jaeger in docker +``` +docker run -d --net fdb-core --name jaeger.local -p 6831:6831/udp -p 16686:16686 jaegertracing/all-in-one:1.14 +``` + +If Docker isn't your cup of tea, the Jaeger project also provides +prebuilt binaries that can be downloaded. On macOS we can easily +setup a development Jaeger instance by running the prebuilt +`jaeger-all-in-one` binary without any arguments.
\ No newline at end of file diff --git a/src/ctrace/rebar.config b/src/ctrace/rebar.config new file mode 100644 index 000000000..362c8785e --- /dev/null +++ b/src/ctrace/rebar.config @@ -0,0 +1,14 @@ +% Licensed 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. + +{cover_enabled, true}. +{cover_print_enabled, true}. diff --git a/src/ctrace/src/ctrace.app.src b/src/ctrace/src/ctrace.app.src new file mode 100644 index 000000000..64f4fc5df --- /dev/null +++ b/src/ctrace/src/ctrace.app.src @@ -0,0 +1,27 @@ +% Licensed 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. + + {application, ctrace, [ + {description, "Open tracer API for CouchDB"}, + {vsn, git}, + {registered, [ + ]}, + {applications, [ + kernel, + stdlib, + syntax_tools, + config, + jaeger_passage, + passage + ]}, + {mod, {ctrace_app, []}} +]}. diff --git a/src/ctrace/src/ctrace.erl b/src/ctrace/src/ctrace.erl new file mode 100644 index 000000000..5521901fd --- /dev/null +++ b/src/ctrace/src/ctrace.erl @@ -0,0 +1,361 @@ +% Licensed 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. + +-module(ctrace). + +-vsn(1). + +-export([ + is_enabled/0, + + with_span/2, + with_span/3, + start_span/1, + start_span/2, + finish_span/0, + finish_span/1, + has_span/0, + external_span/3, + + tag/1, + log/1, + + tags/0, + refs/0, + operation_name/0, + trace_id/0, + span_id/0, + tracer/0, + context/0, + + match/2 +]). + + +-include_lib("couch/include/couch_db.hrl"). +-include_lib("passage/include/opentracing.hrl"). +-include("ctrace.hrl"). + + +-type operation() + :: atom() + | fun(). + +-type tags() + :: #{atom() => term()}. + +-type log_fields() + :: #{atom() => term()}. + +-type start_span_options() + :: [start_span_option()]. + +-type start_span_option() + :: {time, erlang:timespan()} + | {tags, tags()}. + +-type finish_span_options() + :: [finish_span_option()]. + +-type finish_span_option() + :: {time, erlang:timespan()}. + + +-spec is_enabled() -> boolean(). + +is_enabled() -> + case get(?IS_ENABLED_KEY) of + undefined -> + Result = ctrace_config:is_enabled(), + put(?IS_ENABLED_KEY, Result), + Result; + IsEnabled -> + IsEnabled + end. + + +%% @equiv with_span(Operation, [], Fun) +-spec with_span( + Operation :: operation(), + Fun + ) -> Result when + Fun :: fun (() -> Result), + Result :: term(). + +with_span(Operation, Fun) -> + with_span(Operation, #{}, Fun). + +-spec with_span( + Operation :: operation(), + TagsOrOptions :: tags() | start_span_options(), + Fun + ) -> Result when + Fun :: fun (() -> Result), + Result :: term(). + +with_span(Operation, ExtraTags, Fun) when is_map(ExtraTags) -> + with_span(Operation, [{tags, ExtraTags}], Fun); + +with_span(Operation, Options, Fun) -> + try + start_span(Operation, Options), + Fun() + catch Type:Reason -> + Stack = erlang:get_stacktrace(), + log(#{ + ?LOG_FIELD_ERROR_KIND => Type, + ?LOG_FIELD_MESSAGE => Reason, + ?LOG_FIELD_STACK => Stack + }, [error]), + erlang:raise(Type, Reason, Stack) + after + finish_span() + end. + +-spec start_span( + Operation :: operation() + ) -> ok. + +start_span(Operation) -> + start_span(Operation, []). + +-spec start_span( + Operation :: operation(), + Options :: start_span_options() + ) -> ok. + +start_span(Operation, Options) -> + case is_enabled() of + true -> + do_start_span(Operation, Options); + false -> + ok + end. + +do_start_span(Fun, Options) when is_function(Fun) -> + start_span(fun_to_op(Fun), Options); + +do_start_span(OperationName, Options0) -> + Options1 = add_time(Options0), + case passage_pd:current_span() of + undefined -> + put(?ORIGIN_KEY, atom_to_binary(OperationName, utf8)), + Tags = case lists:keyfind(tags, 1, Options0) of + {tags, T} -> + T; + false -> + #{} + end, + case match(OperationName, Tags) of + true -> + Options = [ + {tracer, ?MAIN_TRACER} + | maybe_start_root(Options1) + ], + passage_pd:start_span(OperationName, Options); + false -> + ok + end; + Span -> + Options = add_tags([{child_of, Span} | Options1], #{ + origin => get(?ORIGIN_KEY) + }), + passage_pd:start_span(OperationName, Options) + end. + +-spec finish_span() -> ok. + +finish_span() -> + finish_span([]). + +-spec finish_span( + Options :: finish_span_options() + ) -> ok. + +finish_span(Options0) -> + Options = add_time(Options0), + passage_pd:finish_span(Options). + +-spec tag( + Tags :: tags() + ) -> ok. + +tag(Tags) -> + passage_pd:set_tags(Tags). + +-spec log( + Fields :: log_fields() | fun (() -> log_fields()) + ) -> ok. + +log(FieldsOrFun) -> + log(FieldsOrFun, []). + +log(FieldsOrFun, Options) -> + passage_pd:log(FieldsOrFun, Options). + +-spec tags() -> tags(). + +tags() -> + case passage_pd:current_span() of + undefined -> + undefined; + Span -> + passage_span:get_tags(Span) + end. + +-spec refs() -> passage:refs(). + +refs() -> + case passage_pd:current_span() of + undefined -> + undefined; + Span -> + passage_span:get_refs(Span) + end. + +-spec has_span() -> boolean(). + +has_span() -> + passage_pd:current_span() =/= undefined. + +-spec operation_name() -> atom(). + +operation_name() -> + case passage_pd:current_span() of + undefined -> + undefined; + Span -> + passage_span:get_operation_name(Span) + end. + +-spec trace_id() -> 0..16#FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF. + +trace_id() -> + case passage_pd:current_span() of + undefined -> + undefined; + Span -> + Context = passage_span:get_context(Span), + jaeger_passage_span_context:get_trace_id(Context) + end. + +-spec span_id() -> 0..16#FFFFFFFFFFFFFFFF. + +span_id() -> + case passage_pd:current_span() of + undefined -> + undefined; + Span -> + Context = passage_span:get_context(Span), + jaeger_passage_span_context:get_span_id(Context) + end. + +-spec tracer() -> passage:tracer_id(). + +tracer() -> + case passage_pd:current_span() of + undefined -> + undefined; + Span -> + passage_span:get_tracer(Span) + end. + +-spec context() -> passage_span_contest:context(). + +context() -> + case passage_pd:current_span() of + undefined -> + undefined; + Span -> + passage_span:get_context(Span) + end. + +-spec external_span( + TraceId :: passage:trace_id(), + SpanId :: undefined | passage:span_id(), + ParentSpanId :: undefined | passage:span_id() + ) -> passage:maybe_span(). + +external_span(TraceId, undefined, ParentSpanId) -> + external_span(TraceId, rand:uniform(16#FFFFFFFFFFFFFFFF), ParentSpanId); +external_span(TraceId, SpanId, undefined) -> + external_span(TraceId, SpanId, rand:uniform(16#FFFFFFFFFFFFFFFF)); +external_span(TraceId, SpanId, ParentSpanId) -> + IterFun = fun(Val) -> Val end, + Flags = <<0:32>>, + BaggageItems = <<0:32>>, + Binary = << + TraceId:128, + SpanId:64, + ParentSpanId:64, + Flags/binary, + BaggageItems/binary + >>, + State = {ok, <<"binary">>, Binary, error}, + passage:extract_span(?MAIN_TRACER, binary, IterFun, State). + + +match(OperationId, Tags) -> + OpMod = ctrace_config:filter_module_name(OperationId), + case erlang:function_exported(OpMod, match, 1) of + true -> + do_match(OpMod, Tags); + false -> + AllMod = ctrace_config:filter_module_name("all"), + case erlang:function_exported(AllMod, match, 1) of + true -> do_match(AllMod, Tags); + false -> false + end + end. + + +do_match(Mod, Tags) -> + case Mod:match(Tags) of + true -> + true; + false -> + false; + Rate when is_float(Rate) -> + rand:uniform() =< Rate + end. + + +add_tags(Options, ExtraTags) -> + case lists:keytake(tags, 1, Options) of + {value, {tags, T}, Opts} -> + [{tags, maps:merge(T, ExtraTags)} | Opts]; + false -> + [{tags, ExtraTags} | Options] + end. + +add_time(Options) -> + case lists:keymember(time, 1, Options) of + true -> + Options; + false -> + [{time, os:timestamp()} | Options] + end. + +maybe_start_root(Options) -> + case lists:keytake(root, 1, Options) of + {value, {root, Root}, NewOptions} -> + [{child_of, Root} | NewOptions]; + false -> + Options + end. + +fun_to_op(Fun) -> + {module, M} = erlang:fun_info(Fun, module), + {name, F} = erlang:fun_info(Fun, name), + {arity, A} = erlang:fun_info(Fun, arity), + Str = io_lib:format("~s:~s/~b", [M, F, A]), + list_to_atom(lists:flatten(Str)). diff --git a/src/ctrace/src/ctrace.hrl b/src/ctrace/src/ctrace.hrl new file mode 100644 index 000000000..3819bbd50 --- /dev/null +++ b/src/ctrace/src/ctrace.hrl @@ -0,0 +1,15 @@ +% Licensed 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. + +-define(MAIN_TRACER, jaeger_passage_reporter). +-define(IS_ENABLED_KEY, ctrace_is_enabled). +-define(ORIGIN_KEY, ctrace_origin_key). diff --git a/src/ctrace/src/ctrace_app.erl b/src/ctrace/src/ctrace_app.erl new file mode 100644 index 000000000..c98b897e0 --- /dev/null +++ b/src/ctrace/src/ctrace_app.erl @@ -0,0 +1,26 @@ +% Licensed 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. + +-module(ctrace_app). + +-behaviour(application). + +-export([ + start/2, + stop/1 +]). + +start(_StartType, _StartArgs) -> + ctrace_sup:start_link(). + +stop(_State) -> + ok. diff --git a/src/ctrace/src/ctrace_config.erl b/src/ctrace/src/ctrace_config.erl new file mode 100644 index 000000000..bc2a3dff2 --- /dev/null +++ b/src/ctrace/src/ctrace_config.erl @@ -0,0 +1,133 @@ +% Licensed 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. + +-module(ctrace_config). + +-vsn(1). + +-behaviour(config_listener). + +-export([ + is_enabled/0, + update/0, + + filter_module_name/1 +]). + +-export([ + handle_config_change/5, + handle_config_terminate/3 +]). + +-include("ctrace.hrl"). + + +-spec is_enabled() -> boolean(). +is_enabled() -> + config:get_boolean("tracing", "enabled", false). + + +-spec update() -> ok. +update() -> + case is_enabled() of + true -> + maybe_start_main_tracer(?MAIN_TRACER), + + CompiledFilters = get_compiled_filters(), + + RemovedFilters = lists:foldl(fun({OperationId, FilterDef}, Acc) -> + case compile_filter(OperationId, FilterDef) of + true -> Acc -- [OperationId]; + false -> Acc + end + end, CompiledFilters, config:get("tracing.filters")), + + lists:foreach(fun(OperationId) -> + ModName = filter_module_name(OperationId), + code:delete(ModName), + code:purge(ModName) + end, RemovedFilters), + + case config:get("tracing.filters", "all") of + undefined -> compile_filter("all", "(#{}) -> false"); + _ -> ok + end; + + false -> + jaeger_passage:stop_tracer(?MAIN_TRACER) + end, + ok. + + +-spec filter_module_name(atom() | string()) -> atom(). +filter_module_name(OperationId) when is_atom(OperationId) -> + filter_module_name(atom_to_list(OperationId)); +filter_module_name(OperationId) -> + list_to_atom("ctrace_filter_" ++ OperationId). + + +handle_config_change("tracing", "enabled", _, _Persist, St) -> + update(), + {ok, St}; +handle_config_change("tracing.filters", _Key, _Val, _Persist, St) -> + update(), + {ok, St}; +handle_config_change(_Sec, _Key, _Val, _Persist, St) -> + {ok, St}. + +handle_config_terminate(_Server, _Reason, _State) -> + update(). + + +maybe_start_main_tracer(TracerId) -> + case passage_tracer_registry:get_reporter(TracerId) of + error -> + start_main_tracer(TracerId); + _ -> + true + end. + + +start_main_tracer(TracerId) -> + Sampler = passage_sampler_all:new(), + Options = [ + {thrift_format, + list_to_atom(config:get("tracing", "thrift_format", "compact"))}, + {agent_host, config:get("tracing", "agent_host", "127.0.0.1")}, + {agent_port, config:get_integer("tracing", "agent_port", 6831)}, + {default_service_name, + list_to_atom(config:get("tracing", "app_name", "couchdb"))} + ], + ok = jaeger_passage:start_tracer(TracerId, Sampler, Options). + + +compile_filter(OperationId, FilterDef) -> + try + couch_log:info("Compiling filter : ~s", [OperationId]), + ctrace_dsl:compile(OperationId, FilterDef), + true + catch throw:{error, Reason} -> + couch_log:error("Cannot compile ~s :: ~s~n", [OperationId, Reason]), + false + end. + + +get_compiled_filters() -> + lists:foldl(fun({Mod, _Path}, Acc) -> + ModStr = atom_to_list(Mod), + case ModStr of + "ctrace_filter_" ++ OpName -> + [OpName | Acc]; + _ -> + Acc + end + end, [], code:all_loaded()). diff --git a/src/ctrace/src/ctrace_dsl.erl b/src/ctrace/src/ctrace_dsl.erl new file mode 100644 index 000000000..5e0b0f252 --- /dev/null +++ b/src/ctrace/src/ctrace_dsl.erl @@ -0,0 +1,106 @@ +% Licensed 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. + +-module(ctrace_dsl). +-include_lib("syntax_tools/include/merl.hrl"). + +-export([ + compile/2, + + % Debug + source/2 +]). + + +-type ast() :: erl_syntax:syntaxTree(). + + +-spec compile(OperationId :: string(), FilterDef :: string()) -> ok. +compile(OperationId, FilterDef) -> + AST = parse_filter(OperationId, FilterDef), + merl:compile_and_load(AST), + ok. + + +-spec source(OperationId :: string(), FilterDef :: string()) -> string(). +source(OperationId, FilterDef) -> + AST = parse_filter(OperationId, FilterDef), + Options = [{paper, 160}, {ribbon, 80}], + erl_prettypr:format(erl_syntax:form_list(AST), Options). + + +-spec parse_filter(OperationId :: string(), FilterDef :: string()) -> [ast()]. +parse_filter(OperationId, FilterDef) -> + AST = merl:quote("match" ++ FilterDef ++ "."), + case AST of + ?Q("match(_@Args) when _@__@Guard -> _@Return.") + when erl_syntax:type(Args) == map_expr -> + validate_args(Args), + validate_return(Return), + generate(OperationId, Args, Guard, Return); + ?Q("match(_@Args) when _@__@Guard -> _@@_.") -> + fail("The only argument of the filter should be map"); + ?Q("match(_@@Args) when _@__@Guard -> _@@_.") -> + fail("The arity of the filter function should be 1"); + _ -> + fail("Unknown shape of a filter function") + end. + + +-spec validate_args(MapAST :: ast()) -> ok. +validate_args(MapAST) -> + %% Unfortunatelly merl doesn't seem to support maps + %% so we had to do it manually + lists:foldl(fun(AST, Bindings) -> + erl_syntax:type(AST) == map_field_exact + orelse fail("Only #{field := Var} syntax is supported in the header"), + NameAST = erl_syntax:map_field_exact_name(AST), + erl_syntax:type(NameAST) == atom + orelse fail("Only atoms are supported as field names in the header"), + Name = erl_syntax:atom_value(NameAST), + VarAST = erl_syntax:map_field_exact_value(AST), + erl_syntax:type(VarAST) == variable + orelse fail("Only capitalized names are supported as matching variables in the header"), + Var = erl_syntax:variable_name(VarAST), + maps:is_key(Var, Bindings) + andalso fail("'~s' variable is already in use", [Var]), + Bindings#{Var => Name} + end, #{}, erl_syntax:map_expr_fields(MapAST)). + + +-spec validate_return(Return :: [ast()]) -> ok. +validate_return(Return) -> + case Return of + ?Q("true") -> ok; + ?Q("false") -> ok; + ?Q("_@AST") when erl_syntax:type(AST) == float -> ok; + _ -> + fail("Unsupported return value '~s'", [erl_prettypr:format(Return)]) + end. + + +generate(OperationId, Args, Guard, Return) -> + ModuleName = ctrace_config:filter_module_name(OperationId), + Module = ?Q("-module('@ModuleName@')."), + Export = ?Q("-export([match/1])."), + Function = erl_syntax:function(merl:term(match), [ + ?Q("(_@Args) when _@__@Guard -> _@Return"), + ?Q("(_) -> false") + ]), + lists:flatten([Module, Export, Function]). + + +fail(Msg) -> + throw({error, Msg}). + +fail(Msg, Args) -> + throw({error, lists:flatten(io_lib:format(Msg, Args))}).
\ No newline at end of file diff --git a/src/ctrace/src/ctrace_sup.erl b/src/ctrace/src/ctrace_sup.erl new file mode 100644 index 000000000..70de3c586 --- /dev/null +++ b/src/ctrace/src/ctrace_sup.erl @@ -0,0 +1,41 @@ +% Licensed 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. + +-module(ctrace_sup). +-behaviour(supervisor). +-vsn(1). + +-export([ + start_link/0, + init/1 +]). + +start_link() -> + ctrace_config:update(), + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +init([]) -> + Flags = #{ + strategy => one_for_one, + intensity => 5, + period => 10 + }, + Children = [ + #{ + id => config_listener_mon, + type => worker, + restart => permanent, + shutdown => 5000, + start => {config_listener_mon, start_link, [ctrace_config, nil]} + } + ], + {ok, {Flags, Children}}.
\ No newline at end of file diff --git a/src/ctrace/test/ctrace_config_test.erl b/src/ctrace/test/ctrace_config_test.erl new file mode 100644 index 000000000..0827013fd --- /dev/null +++ b/src/ctrace/test/ctrace_config_test.erl @@ -0,0 +1,153 @@ +% Licensed 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. + +-module(ctrace_config_test). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("ctrace/src/ctrace.hrl"). + + +-define(TDEF(A), {atom_to_list(A), fun A/0}). + + +ctrace_config_test_() -> + { + "Test ctrace_config", + { + setup, + fun setup/0, + fun cleanup/1, + [ + ?TDEF(ensure_main_tracer_started), + ?TDEF(ensure_all_supported), + ?TDEF(handle_all_syntax_error_supported), + ?TDEF(ensure_filter_updated), + ?TDEF(ensure_filter_removed), + ?TDEF(ensure_bad_filter_ignored) + ] + } + }. + + +setup() -> + Ctx = test_util:start_couch([ctrace]), + + config_set("tracing", "enabled", "true"), + + Filter = "(#{method := M}) when M == get -> true", + config_set("tracing.filters", "base", Filter), + + ctrace_config:update(), + + Ctx. + + +cleanup(Ctx) -> + test_util:stop_couch(Ctx). + + +ensure_main_tracer_started() -> + ?assertMatch( + {ok, _}, + passage_tracer_registry:get_reporter(?MAIN_TRACER) + ). + + +ensure_all_supported() -> + config:delete("tracing.filters", "all", false), + test_util:wait_value(fun() -> + config:get("tracing.filters", "all") + end, undefined), + ctrace_config:update(), + + ?assertEqual(false, ctrace:match(bam, #{gee => whiz})), + + Filter = "(#{}) -> true", + config_set("tracing.filters", "all", Filter), + ctrace_config:update(), + + ?assertEqual(true, ctrace:match(bam, #{gee => whiz})). + + +handle_all_syntax_error_supported() -> + couch_log:error("XKCD: TEST START", []), + config:delete("tracing.filters", "all", false), + test_util:wait_value(fun() -> + config:get("tracing.filters", "all") + end, undefined), + ctrace_config:update(), + + ?assertEqual(false, ctrace:match(bam, #{gee => whiz})), + + Filter = "( -> true.", + config_set("tracing.filters", "all", Filter), + ctrace_config:update(), + + % If there's a syntax in the `all` handler + % then we default to not generating traces + ?assertEqual(false, ctrace:match(bam, #{gee => whiz})), + + couch_log:error("XKCD: TEST END", []), + config:delete("tracing.filters", "all", false). + + +ensure_filter_updated() -> + Filter1 = "(#{}) -> true", + config_set("tracing.filters", "bing", Filter1), + ctrace_config:update(), + + ?assertEqual(true, ctrace:match(bing, #{gee => whiz})), + + Filter2 = "(#{}) -> false", + config_set("tracing.filters", "bing", Filter2), + ctrace_config:update(), + + ?assertEqual(false, ctrace:match(bing, #{gee => whiz})). + + +ensure_filter_removed() -> + Filter = "(#{}) -> true", + config_set("tracing.filters", "bango", Filter), + ctrace_config:update(), + + ?assertEqual(true, ctrace:match(bango, #{gee => whiz})), + + config:delete("tracing.filters", "bango", false), + test_util:wait_value(fun() -> + config:get("tracing.filters", "bango") + end, undefined), + ctrace_config:update(), + + FilterMod = ctrace_config:filter_module_name("bango"), + ?assertEqual(false, code:is_loaded(FilterMod)). + + +ensure_bad_filter_ignored() -> + Filter = "#foo stuff", + config_set("tracing.filters", "compile_error", Filter), + ctrace_config:update(), + + FilterMod = ctrace_config:filter_module_name("compile_error"), + ?assertEqual(false, code:is_loaded(FilterMod)), + + AllMod = ctrace_config:filter_module_name(all), + ?assertMatch({file, _}, code:is_loaded(AllMod)). + + +config_set(Section, Key, Value) -> + PrevValue = config:get(Section, Key), + if Value == PrevValue -> ok; true -> + config:set(Section, Key, Value, false), + test_util:wait_other_value(fun() -> + config:get(Section, Key) + end, PrevValue) + end. diff --git a/src/ctrace/test/ctrace_dsl_test.erl b/src/ctrace/test/ctrace_dsl_test.erl new file mode 100644 index 000000000..601e6cd17 --- /dev/null +++ b/src/ctrace/test/ctrace_dsl_test.erl @@ -0,0 +1,123 @@ +% Licensed 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. + +-module(ctrace_dsl_test). + + +-include_lib("eunit/include/eunit.hrl"). + + +simple_parse_and_compile_test() -> + Filter = "(#{'http.method' := Method}) when Method == get -> 1.0", + ctrace_dsl:compile("foo", Filter), + ?assertEqual(1.0, run_filter("foo", #{'http.method' => get})), + ?assertEqual(false, run_filter("foo", #{'httpd.method' => put})). + + +empty_map_test() -> + Filter = "(#{}) -> true", + ctrace_dsl:compile("foo", Filter), + ?assertEqual(true, run_filter("foo", #{})), + ?assertEqual(true, run_filter("foo", #{foo => bar})), + ?assertEqual(false, run_filter("foo", nil)). + + +return_false_test() -> + Filter = "(#{}) -> false", + ctrace_dsl:compile("foo", Filter), + ?assertEqual(false, run_filter("foo", #{})), + ?assertEqual(false, run_filter("foo", nil)). + + +return_float_test() -> + Filter = "(#{}) -> 0.2", + ctrace_dsl:compile("foo", Filter), + ?assertEqual(0.2, run_filter("foo", #{})), + ?assertEqual(false, run_filter("foo", nil)). + + +bad_filter_body_is_list_test() -> + Filter = "(#{}) -> []", + Error = "Unsupported return value '[]'", + ?assertThrow({error, Error}, ctrace_dsl:compile("foo", Filter)). + + +bad_filter_body_has_calls_test() -> + Filter = "(#{}) -> [module:function()]", + Error = "Unsupported return value '[module:function()]'", + ?assertThrow({error, Error}, ctrace_dsl:compile("foo", Filter)). + + +bad_arg_list_too_few_test() -> + Filter = "() -> true", + Error = "The arity of the filter function should be 1", + ?assertThrow({error, Error}, ctrace_dsl:compile("foo", Filter)). + + +bad_arg_list_too_many_test() -> + Filter = "(#{}, foo) -> true", + Error = "The arity of the filter function should be 1", + ?assertThrow({error, Error}, ctrace_dsl:compile("foo", Filter)). + + +bad_arg_type_test() -> + Filters = [ + "(atom) -> true", + "([atom]) -> true", + "(1) -> true", + "(1.0) -> true" + ], + Error = "The only argument of the filter should be map", + lists:foreach(fun(Filter) -> + ?assertThrow({error, Error}, ctrace_dsl:compile("foo", Filter)) + end, Filters). + + +bad_map_association_test() -> + Filter = "(#{foo => Var}) -> true", + Error = "Only #{field := Var} syntax is supported in the header", + ?assertThrow({error, Error}, ctrace_dsl:compile("foo", Filter)). + + +bad_field_variable_test() -> + Filter = "(#{Var := Val}) -> false", + Error = "Only atoms are supported as field names in the header", + ?assertThrow({error, Error}, ctrace_dsl:compile("foo", Filter)). + + +bad_field_match_test() -> + Filter = "(#{foo := 2}) -> true", + Error = "Only capitalized names are supported" + " as matching variables in the header", + ?assertThrow({error, Error}, ctrace_dsl:compile("foo", Filter)). + + +repeated_variable_test() -> + Filter = "(#{foo := Val, bar := Val}) -> true", + Error = "'Val' variable is already in use", + ?assertThrow({error, Error}, ctrace_dsl:compile("foo", Filter)). + + +code_coverage1_test() -> + Filter = "foo(#{}) -> bar", + Error = "Unknown shape of a filter function", + ?assertThrow({error, Error}, ctrace_dsl:compile("foo", Filter)). + + +code_coverage2_test() -> + Filter = "(#{}) -> true", + ?assertMatch([_ | _], ctrace_dsl:source("foo", Filter)). + + +run_filter(OperationId, Value) -> + ModName = ctrace_config:filter_module_name(OperationId), + ModName:match(Value). diff --git a/src/ctrace/test/ctrace_test.erl b/src/ctrace/test/ctrace_test.erl new file mode 100644 index 000000000..962f9aae3 --- /dev/null +++ b/src/ctrace/test/ctrace_test.erl @@ -0,0 +1,412 @@ +% Licensed 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. + +-module(ctrace_test). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("ctrace/src/ctrace.hrl"). + + +-define(TDEF(A), {atom_to_list(A), fun A/0}). + + +ctrace_config_test_() -> + { + "Test ctrace", + { + setup, + fun setup/0, + fun cleanup/1, + [ + ?TDEF(is_enabled_cached), + ?TDEF(simple_with_span), + ?TDEF(with_span_exception), + ?TDEF(simple_start_finish_span), + ?TDEF(op_name_from_fun), + ?TDEF(skipped_when_disabled), + ?TDEF(include_or_skip_on_sampled), + ?TDEF(set_tags_on_start_span), + ?TDEF(set_time_on_start_span), + ?TDEF(skip_on_filtered), + ?TDEF(simple_child_span), + ?TDEF(update_tags), + ?TDEF(update_logs), + ?TDEF(current_span_getters), + ?TDEF(create_external_span), + ?TDEF(use_external_span) + ] + } + }. + + +setup() -> + Ctx = test_util:start_couch([ctrace]), + + config_set("tracing", "enabled", "true"), + + Filter = "(#{}) -> true", + config_set("tracing.filters", "all", Filter), + + ctrace_config:update(), + + MainReporter = passage_tracer_registry:get_reporter(?MAIN_TRACER), + + {MainReporter, Ctx}. + + +cleanup({MainReporter, Ctx}) -> + passage_tracer_registry:set_reporter(?MAIN_TRACER, MainReporter), + test_util:stop_couch(Ctx). + + +is_enabled_cached() -> + erase(?IS_ENABLED_KEY), + Result = ctrace:is_enabled(), + ?assertEqual(Result, get(?IS_ENABLED_KEY)), + ?assert(is_boolean(Result)), + + % Fake override to test that we're using the cached value + put(?IS_ENABLED_KEY, not Result), + ?assertEqual(not Result, ctrace:is_enabled()), + + % Revert to original to not mess with other tests + put(?IS_ENABLED_KEY, Result). + + +simple_with_span() -> + set_self_reporter(), + + Result = ctrace:with_span(zing, fun() -> + a_result + end), + + ?assertEqual(a_result, Result), + + receive + {span, Span} -> + ?assertEqual(zing, passage_span:get_operation_name(Span)) + end. + + +with_span_exception() -> + set_self_reporter(), + + Result = try + ctrace:with_span(zab, fun() -> + throw(foo) + end) + catch T:R -> + {T, R} + end, + + ?assertEqual({throw, foo}, Result), + + receive + {span, Span} -> + ?assertEqual(zab, passage_span:get_operation_name(Span)), + ?assertMatch( + [ + {#{ + 'error.kind' := throw, + event := error, + message := foo, + stack := [_ | _] + }, _TimeStamp} + ], + passage_span:get_logs(Span) + ) + end. + + +simple_start_finish_span() -> + set_self_reporter(), + + ctrace:start_span(foo), + ctrace:finish_span(), + + receive + {span, Span} -> + ?assertEqual(foo, passage_span:get_operation_name(Span)) + end. + + +op_name_from_fun() -> + set_self_reporter(), + + ctrace:start_span(fun ctrace:match/2), + ctrace:finish_span(), + + receive + {span, Span} -> + OpName = passage_span:get_operation_name(Span), + ?assertEqual('ctrace:match/2', OpName) + end. + + +skipped_when_disabled() -> + set_self_reporter(), + + ?assert(not ctrace:has_span()), + ctrace:start_span(foo), + ?assert(ctrace:has_span()), + ctrace:finish_span(), + ?assert(not ctrace:has_span()), + receive {span, _Span} -> ok end, + + IsEnabled = get(?IS_ENABLED_KEY), + try + put(?IS_ENABLED_KEY, false), + + ?assert(not ctrace:has_span()), + ctrace:start_span(foo), + ?assert(not ctrace:has_span()), + ctrace:finish_span(), + ?assert(not ctrace:has_span()) + after + put(?IS_ENABLED_KEY, IsEnabled) + end. + + +set_tags_on_start_span() -> + set_self_reporter(), + + Tags = #{foo => bar}, + ctrace:start_span(bang, [{tags, Tags}]), + ctrace:finish_span(), + + receive + {span, Span} -> + ?assertEqual(bang, passage_span:get_operation_name(Span)), + ?assertEqual(#{foo => bar}, passage_span:get_tags(Span)) + end. + + +set_time_on_start_span() -> + set_self_reporter(), + + Time = os:timestamp(), + timer:sleep(100), + ctrace:start_span(bang, [{time, Time}]), + ctrace:finish_span(), + + receive + {span, Span} -> + ?assertEqual(Time, passage_span:get_start_time(Span)) + end. + + +skip_on_filtered() -> + set_self_reporter(), + + config_set("tracing.filters", "do_skip", "(#{}) -> false"), + ctrace_config:update(), + + ?assert(not ctrace:has_span()), + ctrace:start_span(do_skip), + ?assert(not ctrace:has_span()), + ctrace:finish_span(), + ?assert(not ctrace:has_span()). + + +include_or_skip_on_sampled() -> + set_self_reporter(), + + config_set("tracing.filters", "sample", "(#{}) -> 0.0"), + ctrace_config:update(), + + ?assert(not ctrace:has_span()), + ctrace:start_span(sample), + ?assert(not ctrace:has_span()), + ctrace:finish_span(), + ?assert(not ctrace:has_span()), + + config_set("tracing.filters", "sample", "(#{}) -> 1.0"), + ctrace_config:update(), + + ?assert(not ctrace:has_span()), + ctrace:start_span(sample), + ?assert(ctrace:has_span()), + ctrace:finish_span(), + ?assert(not ctrace:has_span()), + + receive + {span, Span1} -> + ?assertEqual(sample, passage_span:get_operation_name(Span1)) + end, + + config_set("tracing.filters", "sample", "(#{}) -> 0.5"), + ctrace_config:update(), + + ?assert(not ctrace:has_span()), + ctrace:start_span(sample), + IsSampled = ctrace:has_span(), + ctrace:finish_span(), + ?assert(not ctrace:has_span()), + + if not IsSampled -> ok; true -> + receive + {span, Span2} -> + ?assertEqual( + sample, + passage_span:get_operation_name(Span2) + ) + end + end. + + +simple_child_span() -> + set_self_reporter(), + + ctrace:start_span(parent), + ctrace:start_span(child), + ctrace:finish_span(), + ctrace:finish_span(), + + receive + {span, CSpan} -> + ?assertEqual(child, passage_span:get_operation_name(CSpan)) + end, + + receive + {span, PSpan} -> + ?assertEqual(parent, passage_span:get_operation_name(PSpan)) + end. + + +update_tags() -> + set_self_reporter(), + + ctrace:start_span(foo, [{tags, #{foo => bar}}]), + ctrace:tag(#{bango => bongo}), + ctrace:finish_span(), + + receive + {span, Span} -> + ?assertEqual( + #{foo => bar, bango => bongo}, + passage_span:get_tags(Span) + ) + end. + + +update_logs() -> + set_self_reporter(), + + ctrace:start_span(foo), + ctrace:log(#{foo => bar}), + ctrace:finish_span(), + + receive + {span, Span1} -> + ?assertMatch( + [{#{foo := bar}, _TimeStamp}], + passage_span:get_logs(Span1) + ) + end, + + ctrace:start_span(foo), + ctrace:log(fun() -> + #{foo => baz} + end), + ctrace:finish_span(), + + receive + {span, Span2} -> + ?assertMatch( + [{#{foo := baz}, _TimeStamp}], + passage_span:get_logs(Span2) + ) + end. + + +current_span_getters() -> + ?assertEqual(false, ctrace:has_span()), + ?assertEqual(undefined, ctrace:tags()), + ?assertEqual(undefined, ctrace:refs()), + ?assertEqual(undefined, ctrace:operation_name()), + ?assertEqual(undefined, ctrace:trace_id()), + ?assertEqual(undefined, ctrace:span_id()), + ?assertEqual(undefined, ctrace:tracer()), + ?assertEqual(undefined, ctrace:context()), + + ctrace:start_span(parent), + ctrace:start_span(child, [{tags, #{foo => oof}}]), + + ?assertEqual(true, ctrace:has_span()), + ?assertEqual(#{foo => oof, origin => <<"parent">>}, ctrace:tags()), + ?assertMatch([{child_of, _} | _], ctrace:refs()), + ?assertEqual(child, ctrace:operation_name()), + ?assert(is_integer(ctrace:trace_id())), + ?assert(is_integer(ctrace:span_id())), + ?assertEqual(?MAIN_TRACER, ctrace:tracer()), + ?assertNotEqual(undefined, ctrace:context()), + + ctrace:finish_span(), + ctrace:finish_span(), + + receive + {span, CSpan} -> + ?assertEqual(child, passage_span:get_operation_name(CSpan)) + end, + + receive + {span, PSpan} -> + ?assertEqual(parent, passage_span:get_operation_name(PSpan)) + end. + + +create_external_span() -> + Span1 = ctrace:external_span(1, 2, 3), + Ctx1 = passage_span:get_context(Span1), + ?assertEqual(1, jaeger_passage_span_context:get_trace_id(Ctx1)), + ?assertEqual(2, jaeger_passage_span_context:get_span_id(Ctx1)), + + Span2 = ctrace:external_span(42, undefined, undefined), + Ctx2 = passage_span:get_context(Span2), + ?assertEqual(42, jaeger_passage_span_context:get_trace_id(Ctx2)), + ?assert(is_integer(jaeger_passage_span_context:get_span_id(Ctx2))). + + +use_external_span() -> + Parent = ctrace:external_span(1, 2, 3), + + ?assert(not ctrace:has_span()), + ctrace:start_span(foo, [{root, Parent}]), + ?assert(ctrace:has_span()), + ctrace:finish_span(), + ?assert(not ctrace:has_span()), + + receive + {span, Span} -> + Ctx = passage_span:get_context(Span), + TraceId = jaeger_passage_span_context:get_trace_id(Ctx), + ?assertEqual(1, TraceId) + end. + + +config_set(Section, Key, Value) -> + PrevValue = config:get(Section, Key), + if Value == PrevValue -> ok; true -> + config:set(Section, Key, Value, false), + test_util:wait_other_value(fun() -> + config:get(Section, Key) + end, PrevValue) + end. + + +set_self_reporter() -> + SelfReporter = passage_reporter_process:new(self(), span), + passage_tracer_registry:set_reporter(?MAIN_TRACER, SelfReporter), + test_util:wait_value(fun() -> + {ok, Result} = passage_tracer_registry:get_reporter(?MAIN_TRACER), + Result + end, SelfReporter).
\ No newline at end of file |