summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorILYA Khlopotov <iilyak@apache.org>2019-11-12 16:20:47 +0000
committerPaul J. Davis <paul.joseph.davis@gmail.com>2019-11-20 16:58:56 -0600
commite93d1b45064ee76da9603c49bd43552fa12214fe (patch)
treeb0a264ea9165f775e9e759294f9f6ed65eed5ff4
parentc9b8e25eac479fdcbf275ac39323fc355715620d (diff)
downloadcouchdb-e93d1b45064ee76da9603c49bd43552fa12214fe.tar.gz
Add ctrace application
-rw-r--r--.gitignore5
-rw-r--r--rebar.config.script5
-rw-r--r--rel/overlay/etc/default.ini50
-rw-r--r--src/couch/src/couch_util.erl4
-rw-r--r--src/ctrace/README.md291
-rw-r--r--src/ctrace/rebar.config14
-rw-r--r--src/ctrace/src/ctrace.app.src27
-rw-r--r--src/ctrace/src/ctrace.erl361
-rw-r--r--src/ctrace/src/ctrace.hrl15
-rw-r--r--src/ctrace/src/ctrace_app.erl26
-rw-r--r--src/ctrace/src/ctrace_config.erl133
-rw-r--r--src/ctrace/src/ctrace_dsl.erl106
-rw-r--r--src/ctrace/src/ctrace_sup.erl41
-rw-r--r--src/ctrace/test/ctrace_config_test.erl153
-rw-r--r--src/ctrace/test/ctrace_dsl_test.erl123
-rw-r--r--src/ctrace/test/ctrace_test.erl412
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