Hi,

I wanted to run this idea by the ML to see if there is any interest before 
investing time into preparing formal RFC.

# Problem statement

Collecting profiling data is very tricky at the moment. Developers have to run 
generic profiling tools which are not aware of CouchDB specifics. 
This makes it hard to do the performance optimization work. We need a tool 
which would allow us to get profiling data from specific points in the 
codebase. This means code instrumentation. 

# Proposed solution

There is an https://opentracing.io/ project, which is a vendor-neutral APIs and 
instrumentation for distributed tracing. In Erlang it is implemented by 
https://github.com/Bluehouse-Technology/otter library. The library provides a 
nice abstraction to start/finish tracing spans as well as adding tags and log 
entries to a given span. In the context of CouchDB this means that we can do 
something like the following:
- start tracing span on every HTTP request
- add tags to capture additional information such as "database name"/"name of 
endpoint"/"nonce"
- add otter logs in critical parts of the codebase to get profiling data for 
these points.

The otter is the most useful in combination with [zipkin](https://zipkin.io/) 
compatible server such as [jaeger](https://github.com/jaegertracing/jaeger). 
However it can be used even without zipkin. It has a configurable set of 
counters, which makes it possible to get answers on questions like:
- what kind of requests are slow
- if we get a slow request (taking longer then configured threshold) what was 
the trace (annotated with time spent between trace points)
- which function in the trace taking the most time 

# Otter API

The otter has multiple APIs which we would choose on a case by case basis:
- functional API - the span structure need to be passed around (we could extend 
`#http{}`/`#user_ctx{}`/`#db{}`)
- simple process dictionary API - the span data are stored in the process 
dictionary
- Multiple span process dictionary API - supports multiple spans per process
- Span id API - starts a process per span

# Roadblocks

One of the problems we would need to solve is to how to do multiple nodes 
spans. We would need this functionality to trace the request from the HTTP 
endpoint handler running on coordinator to the shard updater process running on 
the storage nodes. 

We could use either:
- extend rexi or fabric to pass and aggregate span information
- pass span info explicitly in every fabric function.

# Quick demo (warning very technical content)

The goal of this demo is to demonstrate the value of otter without zipkin 
server. 

```
diff --git a/rebar.config.script b/rebar.config.script
index c38b6e235..c2b162751 100644
--- a/rebar.config.script
+++ b/rebar.config.script
@@ -129,6 +129,11 @@ OptionalDeps = case WithProper of
         []
 end,

+ManualDeps = [
+  {otter, {url, "https://github.com/Bluehouse-Technology/otter"}, {branch, 
"master"}},
+  {otter_lib, {url, "https://github.com/Bluehouse-Technology/otter_lib"}, 
{branch, "master"}}
+],
+
 BaseUrl = "https://github.com/apache/";,

 MakeDep = fun
@@ -152,7 +157,7 @@ end,
 AddConfig = [
     {require_otp_vsn, "19|20|21|22"},
     {deps_dir, "src"},
-    {deps, lists:map(MakeDep, DepDescs ++ OptionalDeps)},
+    {deps, lists:map(MakeDep, DepDescs ++ OptionalDeps ++ ManualDeps)},
     {sub_dirs, SubDirs},
     {lib_dirs, ["src"]},
     {erl_opts, [{i, "../"} | ErlOpts]},
diff --git a/src/chttpd/src/chttpd.erl b/src/chttpd/src/chttpd.erl
index 1e1d638be..a7aad5010 100644
--- a/src/chttpd/src/chttpd.erl
+++ b/src/chttpd/src/chttpd.erl
@@ -158,6 +158,7 @@ handle_request(MochiReq0) ->

 handle_request_int(MochiReq) ->
     Begin = os:timestamp(),
+    otter_span_pdict_api:start_with_tags("http request", [{"begin_ts", 
Begin}]),
     case config:get("chttpd", "socket_options") of
     undefined ->
         ok;
@@ -233,6 +234,7 @@ handle_request_int(MochiReq) ->

     % put small token on heap to keep requests synced to backend calls
     erlang:put(nonce, Nonce),
+    otter_span_pdict_api:tag("nonce", Nonce),

     % suppress duplicate log
     erlang:put(dont_log_request, true),
@@ -282,6 +284,8 @@ after_request(HttpReq, HttpResp0) ->
         end,
     HttpResp2 = update_stats(HttpReq, HttpResp1),
     chttpd_stats:report(HttpReq, HttpResp2),
+    otter_span_pdict_api:tag("status", HttpResp2#httpd_resp.status),
+    otter_span_pdict_api:log("completed"),
+    otter_span_pdict_api:finish(),
     maybe_log(HttpReq, HttpResp2),
     HttpResp2.

diff --git a/src/fabric/src/fabric.erl b/src/fabric/src/fabric.erl
index 27fa8c045..a1972b445 100644
--- a/src/fabric/src/fabric.erl
+++ b/src/fabric/src/fabric.erl
@@ -56,6 +56,7 @@ all_dbs() ->
 %% @doc returns a list of all database names
 -spec all_dbs(Prefix::iodata()) -> {ok, [binary()]}.
 all_dbs(Prefix) when is_binary(Prefix) ->
+    otter_span_pdict_api:tag("request_type", "_all_dbs"),
     Length = byte_size(Prefix),
     MatchingDbs = mem3:fold_shards(fun(#shard{dbname=DbName}, Acc) ->
         case DbName of
```

```
application:start(otter_lib).
application:start(otter).
f(Rules), Rules = [
  {[
        {greater, otter_span_duration, 10}
    ],[
        {snapshot_count, [long_span], [otter_span_name]},
        send_to_zipkin
   ]}
].
otter_config:write(filter_rules, Rules).
otter:counter_snapshot([long_span,"http request"]).
[{[long_span,"http request"],
  [{snap_timestamp,{2019,9,10,13,46,43,368208}},
   {data,{span,1568123203366286,17299637839902614236,
               "http request",782788946072648712,undefined,
               [{"begin_ts",{1568,123203,366255}},
                {"nonce","a0a1d7c58e"},
                {"status",ok}],
               [],1911}}]}]
```

Unfortunately `counter_snapshot` API doesn't return log events. However this 
problem is very easy to overcome. Here is the example of how to get matching 
spans logged into a log file.

```
diff --git a/src/chttpd/src/chttpd.erl b/src/chttpd/src/chttpd.erl
index 1e1d638be..5f2d60690 100644
--- a/src/chttpd/src/chttpd.erl
+++ b/src/chttpd/src/chttpd.erl
@@ -33,7 +33,7 @@
     send_chunked_error/2, send_json/2,send_json/3,send_json/4,
     validate_ctype/2]).

--export([authenticate_request/3]).
+-export([authenticate_request/3, zipkin_log/2]).

@@ -1207,6 +1216,10 @@ get_user(#httpd{user_ctx = #user_ctx{name = User}}) ->
 get_user(#httpd{user_ctx = undefined}) ->
     "undefined".

+zipkin_log(_URL, EncodedSpans) ->
+    Spans = otter_lib_zipkin_thrift:decode_spans(EncodedSpans),
+    couch_log:error("ZIPKIN :: ~p~n", [Spans]).
+
```

```
otter_config:write(http_client, {chttpd, zipkin_log}).
```

This would produce the log events looking as the following:
```
[error] 2019-09-10T18:01:38.758631Z node1@127.0.0.1 <0.4737.0> -------- ZIPKIN 
:: [{span,1568138498710679,3212384889493927141,<<"http 
request">>,345610195655038913,undefined,[{<<"lc">>,<<>>,{<<"otter_test">>,{127,0,0,1},0}},{<<"begin_ts">>,<<"{1568,138498,710639}">>},{<<"path">>,<<"_all_dbs">>},{<<"method">>,<<"GET">>},{<<"nonce">>,<<"c225c6aef1">>},{<<"status">>,<<"ok">>}],[{1568138498712456,<<"completed">>}],1784}]
```

As you can see the timestamp of the `completed` event is included in the entry. 
Also, since it is a function call we can format the event in any way we like.

# Conclusion

- The otter configuration is simple and presence of `http_client` parameter 
allows us to use otter without zipkin server. 
- The API is simple which makes it possible to replace otter with something 
else if we wouldn't be happy with the implementation.
- The codebase of otter is concise and easy to re-implement in case we would 
need to.

Overall I don't think it will be too complicated to introduce basic 
functionality and extend it latter when we need to.

In case you agree with direction I would appreciate any feedback which would 
help to form requirements for the RFC and PR in the future.

Best regards,
ILYA (aka iilyak)

Reply via email to