You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by da...@apache.org on 2019/11/05 17:26:52 UTC
[couchdb] 01/06: Implement ctrace application
This is an automated email from the ASF dual-hosted git repository.
davisp pushed a commit to branch opentracing-davisp
in repository https://gitbox.apache.org/repos/asf/couchdb.git
commit 2732c28d1c7ebff52230ad29fa26755bdd8a44d2
Author: ILYA Khlopotov <ii...@apache.org>
AuthorDate: Tue Oct 22 13:00:18 2019 +0000
Implement ctrace application
---
.gitignore | 5 +
rebar.config.script | 5 +
src/ctrace/README.md | 330 ++++++++++++++++++++++++++
src/ctrace/src/ctrace.app.src | 27 +++
src/ctrace/src/ctrace.erl | 255 ++++++++++++++++++++
src/ctrace/src/ctrace_action.erl | 38 +++
src/ctrace/src/ctrace_app.erl | 26 ++
src/ctrace/src/ctrace_config.erl | 203 ++++++++++++++++
src/ctrace/src/ctrace_dsl.erl | 202 ++++++++++++++++
src/ctrace/src/ctrace_filter.erl | 45 ++++
src/ctrace/src/ctrace_sampler.erl | 36 +++
src/ctrace/src/ctrace_sup.erl | 43 ++++
src/ctrace/test/exunit/ctrace_config_test.exs | 153 ++++++++++++
src/ctrace/test/exunit/ctrace_dsl_test.exs | 157 ++++++++++++
src/ctrace/test/exunit/ctrace_test.exs | 88 +++++++
src/ctrace/test/exunit/test_helper.exs | 2 +
16 files changed, 1615 insertions(+)
diff --git a/.gitignore b/.gitignore
index 3c8bf0d..2de464c 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 8ef1abc..11cd426 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",
@@ -122,6 +123,10 @@ DepDescs = [
{jiffy, "jiffy", {tag, "CouchDB-0.14.11-2"}},
{mochiweb, "mochiweb", {tag, "v2.19.0"}},
{meck, "meck", {tag, "0.8.8"}},
+{passage, {url, "https://github.com/sile/passage.git"},
+ {tag, "0.2.6"}},
+{jaeger_passage, {url, "https://github.com/sile/jaeger_passage.git"},
+ {tag, "0.1.12"}},
%% TMP - Until this is moved to a proper Apache repo
{erlfdb, "erlfdb", {branch, "master"}}
diff --git a/src/ctrace/README.md b/src/ctrace/README.md
new file mode 100644
index 0000000..3c7e580
--- /dev/null
+++ b/src/ctrace/README.md
@@ -0,0 +1,330 @@
+Notes:
+
+* Filtering on the entire tree
+* Filtering based on time
+* Only report upstream spans when some child span condition is met
+* Stuff
+* Problems with filters
+ - Child spans have already been ignored by the time we get to
+ the end of a parent span
+ - Don't have tags or time at sample time
+ - Rules have to account for every level in the span tree unless
+ its a `(#{}) -> [report]` rule which is not super powerful
+ - Triggering reports based on child spans will lose information
+ on previous child spans since they've already been ignored
+ before a filter condition returns true
+
+* Filtering spans vs not is awkward since it applies to the
+ top level span or something? Do they just pick the point where
+ we start reporting spans?
+
+
+
+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 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
+
+The tracing begins with sampling. We don't do anything if
+sampler is not configured for a given operation.
+
+For every operation we start a separate tracing filter process.
+This process receieve spans when `ctrace:finish_span` is called.
+The filtering processes are named after operation name.
+
+Every filtering process has a number of filtering rules.
+The first rule matching the conditions of a given span would be selected.
+Every rule has a list of actions to execute on given span.
+
+Currently we only support `report`. In the future we might support following:
+
+- report
+- count
+- sample = probability (float)
+
+When the rule is selected and it has `report` action we forward the span to
+`reporter` process. Reporter process encodes the span and sends it to jaeger.
+
+Span pipeline
+-------------
+
++-------+ +------+ +--------+ +------+
+|sampler| ----> |filter| ----> |reporter| ----> |jaeger|
++-------+ +------+ +--------+ +------+
+
+- sampler - adds filtering rules into span and does prefiltering
+- filter - uses filtering rules to decide if it needs to forward span
+- reporter - sends span to jaeger
+
+Code instrumentation
+--------------------
+
+The span lifecycle is controled by
+
+- `ctrace:start_span`
+- `ctrace:finish_span`
+
+The instrumentation can add tags and logs to a span. In some cases we
+embed span in other structures. Therefore to avoid confussion we don't
+use term `span` and use `subject` instead. Currently we support `#httpd{}`
+record and `db` as subjects.
+
+Example of instrumentation:
+```
+HttpReq2 = ctrace:trace(HttpReq1, fun(S0) ->
+ S1 = ctrace:tag(S0, #{
+ peer => Peer,
+ 'http.method' => Method,
+ nonce => Nonce,
+ 'http.url' => Path,
+ 'span.kind' => <<"server">>,
+ component => <<"couchdb.chttpd">>
+ }),
+ ctrace:log(S1, #{
+ field0 => "value0"
+ })
+end``1),
+```
+
+As you can see the `ctrace:trace/2` function receives a function which
+operates on the span. The functions that can be on span are:
+
+- `ctrace:tag/2` to add new tags to the span
+- `ctrace:set_operation_name/2` sometimes operation name is
+ not available when span is started. This function let
+ us set the name latter.
+- `ctrace:log/2` add log event to the span
+
+There are some informative functions as well:
+
+- `ctrace:refs/1` - returns all other spans we have references from the current
+- `ctrace:operation_name/1` - returns operation name for the current span
+- `ctrace:trace_id/1` - returns trace id for the current span
+- `ctrace:span_id/1` - 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 `ctrace:new_request_ctx` to pass additional information
+ about request.
+- Update layers to pass `request_ctx` as needed (not done for jobs).
+- 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
+-------------
+
+The tracers are configured using standard CouchDB ini files
+based configuration. There is a global toggle
+`[tracing]->'enabled' = false` which enables the tracing.
+The samplers are configured in a `[tracing.samplers]` section, which
+specifies the sampler to use for given tracer. If sampler is not
+configured the spans for a given operation are droped. Every sampler
+must have a corespondent filter section. The naming convention is:
+`[tracing.OperationId]`. For security reasons `[tracing.OperationId]`
+is not available via HTTP endpoint. Administrator can toggle tracing
+with predefined rules for specific operation by setting a correspondent
+sampler to either `none` or `all`.
+The keys in filter section are irrelevant and used only for ordering
+purpose. The rules are processed in the alphabetical order. We use a
+DSL for defining rules. The DSL has following structure:
+```
+( #{<[arguments]>} ) when <[conditions]> -> <[actions]>
+```
+
+Where:
+ - 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
+ - guarg_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
+
+### Open tracing agent configuration
+
+```
+[tracing]
+
+thrift_format = compact ; compact | binary
+agent_host = 127.0.0.1
+agent_port = 6831
+; app_name is the value whicj would be used for
+; `location.application` tag
+app_name = couchdb
+```
+
+
+Bellow is an example of a configuration:
+
+```ini
+[tracing]
+enabled = true
+thrift_format = compact ; compact | binary
+agent_host = jaeger.local
+agent_port = 6831
+; app_name is the value whicj would be used for
+; `location.application` tag
+app_name = couchdb
+
+[tracing.samplers]
+
+view.build = all
+database-info.read = all
+
+[tracing.view.build]
+
+a_select = (#{'view.name' := Name}) when Name == "blablabla" -> [report]
+details = (#{parent := Parent}) when Parent == <<"view.build">> -> [report]
+
+[tracing.database-info.read]
+
+select = (#{'http.method' := Method}) when Method == 'GET' -> [report]
+details = (#{parent := Parent}) when Parent == <<"database-info.read">> -> [report]
+```
+
+Note: It is important to add `details = (#{parent := Parent}) when Parent == <<"database-info.read">> -> [report]`
+rule if you wanted to report children spans.
+
+
+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
+```
\ No newline at end of file
diff --git a/src/ctrace/src/ctrace.app.src b/src/ctrace/src/ctrace.app.src
new file mode 100644
index 0000000..64f4fc5
--- /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 0000000..1b7bd88
--- /dev/null
+++ b/src/ctrace/src/ctrace.erl
@@ -0,0 +1,255 @@
+% 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,
+
+ start_span/1,
+ start_span/2,
+ finish_span/0,
+ finish_span/1,
+ with_span/2,
+
+ set_operation_name/1,
+ add_tags/1,
+ log/1,
+ log/2,
+
+ fun_to_op/1,
+
+ get_operation_name/0,
+ get_tags/0,
+ get_refs/0,
+ get_trace_id/0,
+ get_span_id/0,
+ get_tracer/0,
+ get_context/0
+]).
+
+
+-include_lib("passage/include/opentracing.hrl").
+
+-define(ENABLED_KEY, '$ctrace_enabled$').
+
+
+-type tags() :: #{atom() => term()}.
+-type log_fields() :: #{atom() => term()}.
+
+
+-spec is_enabled() -> boolean().
+is_enabled() ->
+ case get(?ENABLED_KEY) of
+ true -> true;
+ false -> false;
+ undefined ->
+ Result = ctrace_config:is_enabled(),
+ put(?ENABLED_KEY, Result),
+ Result
+ end.
+
+
+-spec start_span(OperationName :: atom()) -> ok.
+
+start_span(undefined) ->
+ start_span(ctrace_config:default_tracer(), []);
+
+start_span(OperationName) ->
+ start_span(OperationName, []).
+
+
+-spec start_span(OperationName :: atom(), Options :: [term()]) -> ok.
+
+start_span(OperationName, Options0) ->
+ case is_enabled() of
+ true ->
+ CurrSpan = passage_pd:current_span(),
+ Options1 = case lists:keymember(tracer, 1, Options0) of
+ true -> Options0;
+ false -> [{tracer, jaeger_passage_reporter} | Options0]
+ end,
+ passage_pd:start_span(OperationName, Options1),
+ if CurrSpan == undefined -> ok; true ->
+ ParentOp = passage_span:get_operation_name(CurrSpan),
+ passage_pd:set_tags(#{parent => ParentOp})
+ end;
+ false ->
+ ok
+ end.
+
+
+-spec finish_span() -> ok.
+
+finish_span() ->
+ finish_span([]).
+
+
+-spec finish_span(Options :: [term()]) -> ok.
+
+finish_span(Options) ->
+ case is_enabled() of
+ true ->
+ passage_pd:finish_span(Options);
+ _ ->
+ ok
+ end.
+
+
+%-spec with_span(OperationName :: atom(), Fun :: fun() -> term()) -> term().
+with_span(OperationName, Fun) ->
+ case is_enabled() of
+ true ->
+ try
+ start_span(OperationName, []),
+ 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;
+ false ->
+ Fun()
+ end.
+
+
+-spec set_operation_name(OperationName :: atom()) -> ok.
+
+set_operation_name(OperationName) ->
+ case is_enabled() of
+ true ->
+ passage_pd:set_operation_name(OperationName);
+ _ ->
+ ok
+ end.
+
+
+-spec add_tags(Tags :: tags()) -> ok.
+
+add_tags(Tags) ->
+ case is_enabled() of
+ true ->
+ passage_pd:set_tags(Tags);
+ _ ->
+ ok
+ end.
+
+
+-spec log(Fields :: log_fields()) -> ok.
+
+log(FieldsOrFun) ->
+ log(FieldsOrFun, []).
+
+
+-spec log(Fields :: log_fields(), Options :: [term()]) -> ok.
+
+log(FieldsOrFun, Options) ->
+ case is_enabled() of
+ true ->
+ passage_pd:log(FieldsOrFun, Options);
+ false ->
+ ok
+ 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)).
+
+
+-spec get_tags() -> tags() | undefined.
+
+get_tags() ->
+ case is_enabled() of
+ true ->
+ passage_span:get_tags(passage_pd:current_span());
+ false ->
+ undefined
+ end.
+
+
+-spec get_refs() -> passage:refs() | undefined.
+
+get_refs() ->
+ case is_enabled() of
+ true ->
+ passage_span:get_refs(passage_pd:current_span());
+ false ->
+ undefined
+ end.
+
+
+-spec get_operation_name() -> atom().
+
+get_operation_name() ->
+ case is_enabled() of
+ true ->
+ passage_span:get_operation_name(passage_pd:current_span());
+ false ->
+ undefined
+ end.
+
+
+-spec get_trace_id() -> passage:trace_id() | undefined.
+
+get_trace_id() ->
+ case is_enabled() of
+ true ->
+ jaeger_passage_span_context:get_trace_id(get_context());
+ false ->
+ undefined
+ end.
+
+
+-spec get_span_id() -> passage:span_id() | undefined.
+get_span_id() ->
+ case is_enabled() of
+ true ->
+ jaeger_passage_span_context:get_span_id(get_context());
+ false ->
+ undefined
+ end.
+
+
+-spec get_tracer() -> passage:tracer_id().
+
+get_tracer() ->
+ case is_enabled() of
+ true ->
+ passage_span:get_tracer(passage_pd:current_span());
+ false ->
+ undefined
+ end.
+
+
+-spec get_context() -> passage_span_contest:context().
+
+get_context() ->
+ case is_enabled() of
+ true ->
+ Span = passage_pd:current_span(),
+ passage_span:get_context(Span);
+ false ->
+ undefined
+ end.
diff --git a/src/ctrace/src/ctrace_action.erl b/src/ctrace/src/ctrace_action.erl
new file mode 100644
index 0000000..28276b1
--- /dev/null
+++ b/src/ctrace/src/ctrace_action.erl
@@ -0,0 +1,38 @@
+% 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_action).
+
+-export([
+ sample/1,
+ report/1
+]).
+
+-type action_fun()
+ :: fun((Span :: passage_span:span()) -> boolean()).
+
+-spec sample(
+ [SamplingRate :: float() | integer()]
+ ) -> action_fun().
+
+sample([SamplingRate]) ->
+ fun(_Span) -> rand:uniform() < SamplingRate end.
+
+-spec report(
+ [ReporterId :: passage:tracer_id()]
+ ) -> action_fun().
+
+report([TracerId]) ->
+ fun(Span) ->
+ jaeger_passage_reporter:report(TracerId, Span),
+ true
+ end.
\ No newline at end of file
diff --git a/src/ctrace/src/ctrace_app.erl b/src/ctrace/src/ctrace_app.erl
new file mode 100644
index 0000000..c98b897
--- /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 0000000..b8b45c8
--- /dev/null
+++ b/src/ctrace/src/ctrace_config.erl
@@ -0,0 +1,203 @@
+% 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).
+-behaviour(config_listener).
+-vsn(1).
+
+-export([
+ default_tracer/0,
+ is_enabled/0,
+ update/0
+]).
+
+-export([
+ handle_config_change/5,
+ handle_config_terminate/3
+]).
+
+
+-define(MAIN_TRACER, jaeger_passage_reporter).
+
+
+-spec default_tracer() -> atom().
+
+default_tracer() ->
+ ?MAIN_TRACER.
+
+
+-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) andalso update_config();
+ false ->
+ jaeger_passage:stop_tracer(?MAIN_TRACER)
+ end,
+ ok.
+
+
+handle_config_change("tracing.samplers", OperationIdStr, Value, _, St) ->
+ case is_enabled() of
+ true -> update_sampler(OperationIdStr, Value);
+ false -> ok
+ end,
+ {ok, St};
+handle_config_change("tracing." ++ OperationIdStr, _Key, _Val, _Persist, St) ->
+ case is_enabled() of
+ true -> update_sampler(OperationIdStr);
+ false -> ok
+ end,
+ {ok, St};
+handle_config_change("tracing", "enabled", _, _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) ->
+ Format = list_to_atom(config:get("tracing", "thrift_format", "compact")),
+ Host = config:get("tracing", "agent_host", "127.0.0.1"),
+ Port = config:get_integer("tracing", "agent_port", 6831),
+ Name = list_to_atom(config:get("tracing", "app_name", "couchdb")),
+
+ Sampler = passage_sampler_all:new(),
+ Options = [
+ {thrift_format, Format},
+ {agent_host, Host},
+ {agent_port, Port},
+ {default_service_name, Name}
+ ],
+
+ case jaeger_passage:start_tracer(TracerId, Sampler, Options) of
+ ok ->
+ true;
+ {error, Reason} ->
+ couch_log:error("Cannot start main tracer: ~p~n", [Reason]),
+ false
+ end.
+
+
+update_config() ->
+ lists:foreach(fun({OperationIdStr, SamplerDef}) ->
+ update_sampler(OperationIdStr, SamplerDef)
+ end, config:get("tracing.samplers")).
+
+
+update_sampler(OperationIdStr) when is_list(OperationIdStr) ->
+ case config:get("tracing.samplers", OperationIdStr) of
+ undefined ->
+ rem_tracer(OperationIdStr);
+ SamplerDef ->
+ update_sampler(OperationIdStr, SamplerDef)
+ end.
+
+
+update_sampler(OperationIdStr, deleted) ->
+ rem_tracer(OperationIdStr);
+
+update_sampler(OperationIdStr, SamplerDef) ->
+ case parse_sampler(SamplerDef) of
+ undefined ->
+ rem_tracer(OperationIdStr);
+ Sampler ->
+ compile_rules(OperationIdStr),
+ add_tracer(OperationIdStr, Sampler)
+ end.
+
+
+add_tracer(OperationIdStr, Sampler) ->
+ OperationId = list_to_atom(OperationIdStr),
+ case passage_tracer_registry:get_reporter(OperationId) of
+ {ok, _} ->
+ % Only need to update the sampler here as the
+ % ctrace_filter will automatically update to use
+ % the recompiled dynamic module.
+ passage_tracer_registry:set_sampler(OperationId, Sampler);
+ error ->
+ Mod = filter_module_name(OperationIdStr),
+ CTraceFilter = ctrace_filter:new(OperationId, Mod),
+ Filter = passage_reporter:new(ctrace_filter, CTraceFilter),
+ Ctx = jaeger_passage_span_context,
+ passage_tracer_registry:register(OperationId, Ctx, Sampler, Filter)
+ end.
+
+
+rem_tracer(OperationIdStr) ->
+ OperationId = list_to_atom(OperationIdStr),
+ passage_tracer_registry:deregister(OperationId).
+
+
+compile_rules(OperationIdStr) ->
+ OperationId = list_to_atom(OperationIdStr),
+ FilterMod = filter_module_name(OperationIdStr),
+ RulesRaw = config:get("tracing." ++ OperationIdStr),
+ try
+ Rules = lists:map(fun({Name, RuleDef}) ->
+ Rule = ctrace_dsl:parse_rule(Name, RuleDef),
+ RawActions = maps:get(actions, Rule),
+ Actions = lists:map(fun set_action/1, RawActions),
+ maps:put(actions, Actions, Rule)
+ end, RulesRaw),
+ ctrace_dsl:compile(FilterMod, Rules)
+ catch throw:{error, Reason} ->
+ rem_tracer(OperationIdStr),
+ couch_log:error("cannot compile '~s': ~p~n", [OperationId, Reason])
+ end.
+
+
+parse_sampler(Binary) when is_binary(Binary) ->
+ parse_sampler(binary_to_list(Binary));
+parse_sampler("all") ->
+ ctrace_sampler:all();
+parse_sampler("null") ->
+ ctrace_sampler:null();
+parse_sampler(FloatStr) ->
+ Help = "Cannot parse sampler. The only supported formats are: "
+ " all | null | float(), got '~s'",
+ try
+ ctrace_sampler:probalistic(binary_to_float(FloatStr))
+ catch _:_ ->
+ couch_log:error(Help, [FloatStr]),
+ undefined
+ end.
+
+
+set_action({sample, Rate}) ->
+ {ctrace_action, sample, [Rate]};
+set_action(report) ->
+ {ctrace_action, report, [?MAIN_TRACER]}.
+
+
+filter_module_name(OperationIdStr) ->
+ list_to_atom("ctrace_filter_" ++ OperationIdStr).
diff --git a/src/ctrace/src/ctrace_dsl.erl b/src/ctrace/src/ctrace_dsl.erl
new file mode 100644
index 0000000..efe9545
--- /dev/null
+++ b/src/ctrace/src/ctrace_dsl.erl
@@ -0,0 +1,202 @@
+% 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([
+ parse_rule/2,
+ generate/2,
+ compile/2,
+ source/1,
+ print_source/1
+]).
+
+-type ast()
+ :: erl_syntax:syntaxTree().
+
+-type bindings()
+ :: map().
+
+-type action()
+ :: report
+ | {sample, integer}
+ | {sample, float}.
+
+-type rule()
+ :: #{
+ name := atom(),
+ args := ast(),
+ conditions := ast(),
+ bindings := bindings(),
+ actions := [action()],
+ source := string()
+ }.
+
+-spec parse_rule(
+ FunName :: string(),
+ String :: string()
+ ) -> rule().
+
+parse_rule(FunName, String) ->
+ AST = merl:quote(FunName ++ String ++ "."),
+ case AST of
+ ?Q("'@Name'(_@Args) when _@__@Guard -> [_@@Actions].")
+ when erl_syntax:type(Args) == map_expr ->
+ #{
+ name => erl_syntax:atom_value(Name),
+ args => Args,
+ conditions => Guard,
+ bindings => bindings(Args),
+ actions => parse_actions(Actions),
+ source => String
+ };
+ ?Q("'@Name'(_@Args) when _@__@Guard -> _@@_.")
+ when erl_syntax:type(Args) == map_expr ->
+ fail("Function body should be a list of actions");
+ ?Q("'@Name'(_@Args) when _@__@Guard -> _@@_.") ->
+ fail("The only argument of the filter should be map");
+ ?Q("'@Name'(_@@Args) when _@__@Guard -> _@@_.") ->
+ fail("The arrity of the filter function should be 1");
+ _ ->
+ fail("Unknown shape of a filter function")
+ end.
+
+-spec bindings(
+ MapAST :: ast()
+ ) -> bindings().
+
+bindings(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(io_libs:format("'~s' variable is already in use", [Var])),
+ Bindings#{Var => Name}
+ end, #{}, erl_syntax:map_expr_fields(MapAST)).
+
+-spec parse_actions(
+ Actions :: [ast()]
+ ) -> [action()].
+
+parse_actions(Actions) ->
+ lists:map(fun(ActionAST) ->
+ parse_action(ActionAST)
+ end, Actions).
+
+-spec parse_action(
+ Actions :: ast()
+ ) -> action().
+
+parse_action(ActionAST) ->
+ case ActionAST of
+ ?Q("report") ->
+ report;
+ ?Q("sample(_@AST)") when erl_syntax:type(AST) == integer ->
+ {sample, erl_syntax:integer_value(AST)};
+ ?Q("sample(_@AST)") when erl_syntax:type(AST) == float ->
+ {sample, erl_syntax:float_value(AST)};
+ ?Q("sample(_@AST)") ->
+ fail("expecting `integer | float` in `sample` action");
+ _ ->
+ fail(lists:flatten(io_lib:format(
+ "unsuported action '~s'", [erl_prettypr:format(ActionAST)])))
+ end.
+
+-spec generate(
+ ModuleName :: module(),
+ Rules :: [rule()]
+ ) -> [ast()].
+
+generate(ModuleName, Rules) ->
+ Module = ?Q("-module('@ModuleName@')."),
+ Export = ?Q("-export([match/1])."),
+ Ordered = order_rules(Rules),
+ Functions = [
+ erl_syntax:function(merl:term(match), [
+ function_clause(Rule)
+ || Rule <- Ordered] ++ [?Q("(_) -> false")])
+ ],
+ lists:flatten([Module, Export, Functions]).
+
+-spec order_rules(
+ [rule()]
+ ) -> [rule()].
+
+order_rules(Rules) ->
+ lists:sort(fun(RuleA, RuleB) ->
+ maps:get(name, RuleA) < maps:get(name, RuleB)
+ end, Rules).
+
+-spec source(
+ Forms :: ast()
+ ) -> string().
+
+source(Forms) ->
+ erl_prettypr:format(
+ erl_syntax:form_list(Forms),
+ [{paper,160},{ribbon,80}]).
+
+-spec print_source(
+ ast()
+ ) -> ok.
+
+print_source(Forms) ->
+ io:format(source(Forms) ++ "~n", []).
+
+-spec function_clause(
+ rule()
+ ) -> ast().
+
+function_clause(Rule) ->
+ #{
+ args := Args,
+ conditions := Guard,
+ actions := Actions
+ } = Rule,
+ ActionsAST = actions(Actions),
+ ?Q("(_@Args) when _@__@Guard -> _@ActionsAST").
+
+-spec actions(
+ [mfa()]
+ ) -> ast().
+
+actions(Actions) ->
+ erl_syntax:list(lists:map(fun({Module, Function, Args}) ->
+ %% keep in mind that implementation function would
+ %% receive a list of arguments
+ %% i.e. you would need to implement it as
+ %% function_name([Arg1, Arg2]) -> fun(Span) -> true end).
+ ?Q("'@Module@':'@Function@'(_@@Args@)")
+ end, Actions)).
+
+-spec compile(
+ Module :: module(),
+ Rules :: [mfa()]
+ ) -> term().
+
+compile(Module, Rules) ->
+ AST = generate(Module, Rules),
+ merl:compile_and_load(AST, [verbose]).
+
+fail(Msg) ->
+ throw({error, Msg}).
diff --git a/src/ctrace/src/ctrace_filter.erl b/src/ctrace/src/ctrace_filter.erl
new file mode 100644
index 0000000..0feefcf
--- /dev/null
+++ b/src/ctrace/src/ctrace_filter.erl
@@ -0,0 +1,45 @@
+% 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_filter).
+-include_lib("passage/include/opentracing.hrl").
+
+-behaviour(passage_reporter).
+
+-export([
+ new/2,
+ report/2,
+ module/1
+]).
+
+
+new(OperationId, Module) ->
+ {OperationId, Module}.
+
+
+report({_OperationId, Module}, PSpan) ->
+ Tags = passage_span:get_tags(PSpan),
+ try
+ case Module:match(Tags) of
+ false ->
+ ok;
+ Actions ->
+ lists:takewhile(fun(Action) -> Action(PSpan) end, Actions),
+ ok
+ end
+ catch error:undef ->
+ ok
+ end.
+
+
+module({_OperationId, Module}) ->
+ Module.
diff --git a/src/ctrace/src/ctrace_sampler.erl b/src/ctrace/src/ctrace_sampler.erl
new file mode 100644
index 0000000..7262557
--- /dev/null
+++ b/src/ctrace/src/ctrace_sampler.erl
@@ -0,0 +1,36 @@
+% 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_sampler).
+
+-export([
+ all/0,
+ null/0,
+ probalistic/1
+]).
+
+-spec all() -> passage_sampler:sampler().
+
+all() ->
+ passage_sampler_all:new().
+
+-spec null() -> passage_sampler:sampler().
+
+null() ->
+ passage_sampler_null:new().
+
+-spec probalistic(
+ Rate :: float()
+ ) -> passage_sampler:sampler().
+
+probalistic(SamplingRate) ->
+ passage_sampler_probalistic:new(SamplingRate).
diff --git a/src/ctrace/src/ctrace_sup.erl b/src/ctrace/src/ctrace_sup.erl
new file mode 100644
index 0000000..0a1e50b
--- /dev/null
+++ b/src/ctrace/src/ctrace_sup.erl
@@ -0,0 +1,43 @@
+% 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}}.
diff --git a/src/ctrace/test/exunit/ctrace_config_test.exs b/src/ctrace/test/exunit/ctrace_config_test.exs
new file mode 100644
index 0000000..408497a
--- /dev/null
+++ b/src/ctrace/test/exunit/ctrace_config_test.exs
@@ -0,0 +1,153 @@
+defmodule Couch.CTrace.Config.Test do
+ require Logger
+ use ExUnit.Case
+ @moduletag capture_log: true
+
+ setup do
+ apps = :test_util.start_applications([:ctrace])
+ :meck.new(:couch_log, [{:stub_all, :meck.val(:ok)}])
+
+ on_exit(fn ->
+ :test_util.stop_applications(apps)
+ :meck.unload()
+ end)
+
+ :config.set('tracing.samplers', 'all-docs', 'all', false)
+
+ :config.set(
+ 'tracing.all-docs',
+ 'all',
+ ~C"(#{method := M}) when M == get -> []",
+ false
+ )
+
+ :config.set_boolean('tracing', 'enabled', true, false)
+
+ {:ok, reporter} =
+ wait_non_error(fn ->
+ :passage_tracer_registry.get_reporter(:"all-docs")
+ end)
+
+ filter = :passage_reporter.get_state(reporter)
+ %{filter: :ctrace_filter.module(filter)}
+ end
+
+ describe "Supervision tree :" do
+ test "main jaeger reporter is started" do
+ assert match?(
+ {:ok, _},
+ :passage_tracer_registry.get_reporter(:jaeger_passage_reporter)
+ )
+ end
+
+ test "pre-configured reporter is started" do
+ assert match?(
+ {:ok, _},
+ :passage_tracer_registry.get_reporter(:"all-docs")
+ )
+ end
+
+ test "reporter is started on config change" do
+ :config.set('tracing.samplers', 'bulk', 'all', false)
+ :config.set('tracing.bulk', 'all', ~C"(#{}) -> [report]", false)
+
+ assert wait_non_error(fn ->
+ :passage_tracer_registry.get_reporter(:bulk)
+ end)
+ end
+
+ test "reporter is stoped when deleted" do
+ assert wait_non_error(fn ->
+ :passage_tracer_registry.get_reporter(:"all-docs")
+ end) != :timeout
+
+ :config.delete('tracing.samplers', 'all-docs', false)
+
+ assert wait_error(fn ->
+ :passage_tracer_registry.get_reporter(:"all-docs")
+ end) != :timeout
+ end
+ end
+
+ describe "Configuration :" do
+ test "recompile rules on config update", %{filter: module} do
+ assert match?([], module.match(%{method: :get}))
+ assert match?(false, module.match(%{method: :post}))
+
+ :config.set(
+ 'tracing.all-docs',
+ 'all',
+ ~C"(#{method := M}) when M == post -> []",
+ false
+ )
+
+ assert match?(
+ false,
+ :test_util.wait_other_value(
+ fn ->
+ module.match(%{method: :get})
+ end,
+ []
+ )
+ )
+
+ assert match?([], module.match(%{method: :post}))
+ end
+
+ test "log errors", %{filter: _} do
+ :config.set('tracing.all-docs', 'all', ~C"( -> syntax_error", false)
+ :ctrace_config.update()
+
+ [error | _] =
+ :test_util.wait_other_value(
+ fn ->
+ capture_logs(:error, ~r"cannot compile '")
+ end,
+ []
+ )
+
+ assert [:"all-docs", '1: syntax error before: \'->\''] == error
+ end
+ end
+
+ describe "Matching :" do
+ test "should match", %{filter: module} do
+ assert match?([], module.match(%{method: :get}))
+ end
+
+ test "should not match", %{filter: module} do
+ assert match?(false, module.match(%{method: :post}))
+ end
+ end
+
+ defp wait_error(fun) do
+ :test_util.wait_value(fun, :error)
+ end
+
+ defp wait_non_error(fun) do
+ :test_util.wait_other_value(fun, :error)
+ end
+
+ defp capture_logs(level, regexp) do
+ history(:couch_log, level, 2)
+ |> Enum.flat_map(fn event ->
+ {_, _, [msg, args]} = elem(event, 1)
+
+ if Regex.match?(regexp, List.to_string(msg)) do
+ [args]
+ else
+ []
+ end
+ end)
+ end
+
+ defp history(module, function, arity) do
+ history = :meck.history(module)
+
+ history
+ |> Enum.filter(fn event ->
+ {_, fun, args} = elem(event, 1)
+ function == fun and arity == length(args)
+ end)
+ end
+end
diff --git a/src/ctrace/test/exunit/ctrace_dsl_test.exs b/src/ctrace/test/exunit/ctrace_dsl_test.exs
new file mode 100644
index 0000000..f5e2947
--- /dev/null
+++ b/src/ctrace/test/exunit/ctrace_dsl_test.exs
@@ -0,0 +1,157 @@
+defmodule Couch.CTrace.DSL.Test do
+ require Logger
+ use ExUnit.Case, async: true
+ @filter_module List.to_atom(Atom.to_charlist(__MODULE__) ++ '_filter')
+ @moduletag capture_log: true
+
+ describe "DSL roundtrip :" do
+ test "Simple Parse and Compile" do
+ rule_str = ~C"""
+ (#{'http.method' := Method}) when Method == get -> [sample(1.0)]
+ """
+
+ rule = :ctrace_dsl.parse_rule('get', rule_str)
+ rule = set_actions(rule)
+ ast = generate([rule])
+ :merl.compile_and_load(ast, [:verbose])
+ end
+ end
+
+ describe "DSL compiler :" do
+ test "match clauses are in alphabetical order" do
+ rule_str_a = ~C"(#{foo := A}) when A == 1 -> [sample(1)]"
+ rule_str_b = ~C"(#{foo := B}) when B == 2 -> [sample(2)]"
+ rule_a = :ctrace_dsl.parse_rule('a', rule_str_a)
+ rule_b = :ctrace_dsl.parse_rule('b', rule_str_b)
+ set_action = fn {:sample, rate} -> {__MODULE__, :as_is, [rate]} end
+ rule_a = set_actions(rule_a, set_action)
+ rule_b = set_actions(rule_b, set_action)
+
+ ast = generate([rule_b, rule_a])
+ :merl.compile_and_load(ast, [:verbose])
+ assert match?({[[1]], [[2]]}, {match(%{foo: 1}), match(%{foo: 2})})
+ :code.delete(@filter_module)
+ :code.purge(@filter_module)
+
+ ast = generate([rule_a, rule_b])
+ :merl.compile_and_load(ast, [:verbose])
+ assert match?({[[1]], [[2]]}, {match(%{foo: 1}), match(%{foo: 2})})
+ end
+ end
+
+ describe "DSL parsing :" do
+ test "empty map" do
+ rule_str = ~C"(#{}) -> [report]"
+ assert match?(%{}, parse(rule_str))
+ end
+
+ test "empty actions" do
+ rule_str = ~C"(#{}) -> []"
+ assert match?(%{}, parse(rule_str))
+ end
+ end
+
+ describe "DSL parsing error handling :" do
+ test "body is not a list" do
+ rule_str = ~C"(#{}) -> hello"
+ error_str = 'Function body should be a list of actions'
+
+ assert catch_throw(parse(rule_str)) == {:error, error_str}
+ end
+
+ test "body contains calls" do
+ rule_str = ~C"(#{}) -> [module:function()]"
+ error_str = ~C"unsuported action 'module:function()'"
+
+ assert catch_throw(parse(rule_str)) == {:error, error_str}
+ end
+
+ test "less than one argument" do
+ rule_str = ~C"() -> [report]"
+ error_str = ~C"The arrity of the filter function should be 1"
+
+ assert catch_throw(parse(rule_str)) == {:error, error_str}
+ end
+
+ test "more than one argument" do
+ rule_str = ~C"(#{}, foo) -> [report]"
+ error_str = ~C"The arrity of the filter function should be 1"
+
+ assert catch_throw(parse(rule_str)) == {:error, error_str}
+ end
+
+ test "argument is not a map (atom)" do
+ rule_str = ~C"(atom) -> [report]"
+ error_str = ~C"The only argument of the filter should be map"
+
+ assert catch_throw(parse(rule_str)) == {:error, error_str}
+ end
+
+ test "argument is not a map (list)" do
+ rule_str = ~C"([atom]) -> [report]"
+ error_str = ~C"The only argument of the filter should be map"
+
+ assert catch_throw(parse(rule_str)) == {:error, error_str}
+ end
+
+ test "argument is not a map (integer)" do
+ rule_str = ~C"(1) -> [report]"
+ error_str = ~C"The only argument of the filter should be map"
+
+ assert catch_throw(parse(rule_str)) == {:error, error_str}
+ end
+
+ test "argument is not a map (float)" do
+ rule_str = ~C"(1.0) -> [report]"
+ error_str = ~C"The only argument of the filter should be map"
+
+ assert catch_throw(parse(rule_str)) == {:error, error_str}
+ end
+ end
+
+ defp parse(rule) do
+ :ctrace_dsl.parse_rule('test', rule)
+ end
+
+ defp set_actions(%{} = rule) do
+ set_actions(rule, &set_action/1)
+ end
+
+ defp set_actions(%{:actions => actions} = rule, map_fun) do
+ actions =
+ actions
+ |> Enum.map(map_fun)
+
+ %{rule | actions: actions}
+ end
+
+ defp set_action({:sample, rate}) do
+ {__MODULE__, :sample, [rate]}
+ end
+
+ defp set_action(:report) do
+ {__MODULE__, :report, []}
+ end
+
+ def as_is(arg) do
+ arg
+ end
+
+ def sample(_rate) do
+ fn _ -> true end
+ end
+
+ def report() do
+ fn _ -> true end
+ end
+
+ def generate(rules) do
+ ast = :ctrace_dsl.generate(@filter_module, rules)
+ Logger.debug(fn -> "Generated module:\n#{:ctrace_dsl.source(ast)}\n" end)
+ ast
+ end
+
+ def match(tags) do
+ @filter_module.match(tags)
+ end
+end
diff --git a/src/ctrace/test/exunit/ctrace_test.exs b/src/ctrace/test/exunit/ctrace_test.exs
new file mode 100644
index 0000000..cc46a40
--- /dev/null
+++ b/src/ctrace/test/exunit/ctrace_test.exs
@@ -0,0 +1,88 @@
+defmodule Couch.CTrace.Test do
+ require Logger
+ use ExUnit.Case
+ @moduletag capture_log: true
+
+ setup do
+ apps = :test_util.start_applications([:ctrace])
+ :meck.new(:couch_log, [{:stub_all, :meck.val(:ok)}])
+ :meck.new(:jaeger_passage_reporter, [:passthrough])
+ :meck.expect(:jaeger_passage_reporter, :report, fn _, _ -> :ok end)
+
+ on_exit(fn ->
+ :test_util.stop_applications(apps)
+ :meck.unload()
+ end)
+
+ :config.set('tracing.samplers', 'all-docs', 'all', false)
+ :config.set('tracing.all-docs', 'all', ~C"(#{}) -> [report]", false)
+ :config.set_boolean('tracing', 'enabled', true, false)
+
+ {:ok, reporter} =
+ :test_util.wait_other_value(
+ fn ->
+ :passage_tracer_registry.get_reporter(:"all-docs")
+ end,
+ :error
+ )
+
+ filter = :passage_reporter.get_state(reporter)
+ %{filter: :ctrace_filter.module(filter)}
+ end
+
+ describe "Basic : " do
+ test "spans are reported" do
+ :ctrace.start_span(:"all-docs")
+ :ctrace.finish_span()
+
+ assert length(reports()) == 1
+ end
+
+ test "child spans are reported" do
+ :ctrace.start_span(:"all-docs")
+ :ctrace.start_span(:"child-span")
+ :ctrace.finish_span()
+ :ctrace.finish_span()
+
+ assert length(reports()) == 2
+ end
+ end
+
+ defp reports() do
+ events =
+ :meck.history(:jaeger_passage_reporter)
+ |> Enum.filter(fn event ->
+ {_, fun, _} = elem(event, 1)
+ fun == :report
+ end)
+
+ events
+ |> Enum.flat_map(fn event ->
+ {_, _, args} = elem(event, 1)
+ [args]
+ end)
+ end
+
+ defp capture_logs(level, regexp) do
+ history(:couch_log, level, 2)
+ |> Enum.flat_map(fn event ->
+ {_, _, [msg, args]} = elem(event, 1)
+
+ if Regex.match?(regexp, List.to_string(msg)) do
+ [args]
+ else
+ []
+ end
+ end)
+ end
+
+ defp history(module, function, arity) do
+ history = :meck.history(module)
+
+ history
+ |> Enum.filter(fn event ->
+ {_, fun, args} = elem(event, 1)
+ function == fun and arity == length(args)
+ end)
+ end
+end
diff --git a/src/ctrace/test/exunit/test_helper.exs b/src/ctrace/test/exunit/test_helper.exs
new file mode 100644
index 0000000..3140500
--- /dev/null
+++ b/src/ctrace/test/exunit/test_helper.exs
@@ -0,0 +1,2 @@
+ExUnit.configure(formatters: [JUnitFormatter, ExUnit.CLIFormatter])
+ExUnit.start()