----------------------------------------- The HAProxy OpenTracing filter (OT) Version 1.0 ( Last update: 2020-12-10 ) ----------------------------------------- Author : Miroslav Zagorac Contact : mzagorac at haproxy dot com SUMMARY -------- 0. Terms 1. Introduction 2. Build instructions 3. Basic concepts in OpenTracing 4. OT configuration 4.1. OT scope 4.2. "ot-tracer" section 4.3. "ot-scope" section 4.4. "ot-group" section 5. Examples 5.1 Benchmarking results 6. OT CLI 7. Known bugs and limitations 0. Terms --------- * OT: The HAProxy OpenTracing filter OT is the HAProxy filter that allows you to send data to distributed tracing systems via the OpenTracing API. 1. Introduction ---------------- Nowadays there is a growing need to divide a process into microservices and there is a problem of monitoring the work of the same process. One way to solve this problem is to use distributed tracing service in a central location, the so-called tracer. OT is a feature introduced in HAProxy 2.4. This filter enables communication via the OpenTracing API with OpenTracing compatible servers (tracers). Currently, tracers that support this API include Datadog, Jaeger, LightStep and Zipkin. The OT filter was primarily tested with the Jaeger tracer, while configurations for both Datadog and Zipkin tracers were also set in the test directory. The OT filter is a standard HAProxy filter, so what applies to others also applies to this one (of course, by that I mean what is described in the documentation, more precisely in the doc/internals/filters.txt file). The OT filter activation is done explicitly by specifying it in the HAProxy configuration. If this is not done, the OT filter in no way participates in the work of HAProxy. As for the impact on HAProxy speed, this is documented with several tests located in the test directory, and the result is found in the README-speed-* files. In short, the speed of operation depends on the way it is used and the complexity of the configuration, from an almost immeasurable impact to a significant deceleration (5x and more). I think that in some normal use the speed of HAProxy with the filter on will be quite satisfactory with a slowdown of less than 4% (provided that no more than 10% of requests are sent to the tracer, which is determined by the keyword 'rate-limit'). The OT filter allows intensive use of ACLs, which can be defined anywhere in the configuration. Thus, it is possible to use the filter only for those connections that are of interest to us. 2. Build instructions ---------------------- OT is the HAProxy filter and as such is compiled together with HAProxy. To communicate with some OpenTracing compatible tracer, the OT filter uses the OpenTracing C Wrapper library (which again uses the OpenTracing CPP library). This means that we must have both libraries installed on the system on which we want to compile or use HAProxy. Instructions for compiling and installing both required libraries can be found at https://github.com/haproxytech/opentracing-c-wrapper . Also, to use the OT filter when running HAProxy we need to have an OpenTracing plugin for the tracer we want to use. We will return to this later, in section 5. The OT filter can be more easily compiled using the pkg-config tool, if we have the OpenTracing C Wrapper library installed so that it contains pkg-config files (which have the .pc extension). If the pkg-config tool cannot be used, then the path to the directory where the include files and libraries are located can be explicitly specified. Below are examples of the two ways to compile HAProxy with the OT filter, the first using the pkg-congfig tool and the second explicitly specifying the path to the OpenTracing C Wrapper include and library. Note: prompt '%' indicates that the command is executed under a unprivileged user, while prompt '#' indicates that the command is executed under the root user. Example of compiling HAProxy using the pkg-congfig tool (assuming the OpenTracing C Wrapper library is installed in the /opt directory): % PKG_CONFIG_PATH=/opt/lib/pkgconfig make USE_OT=1 TARGET=linux-glibc The OT filter can also be compiled in debug mode as follows: % PKG_CONFIG_PATH=/opt/lib/pkgconfig make USE_OT=1 OT_DEBUG=1 TARGET=linux-glibc HAProxy compilation example explicitly specifying path to the OpenTracing C Wrapper include and library: % make USE_OT=1 OT_INC=/opt/include OT_LIB=/opt/lib TARGET=linux-glibc In case we want to use debug mode, then it looks like this: % make USE_OT=1 OT_DEBUG=1 OT_INC=/opt/include OT_LIB=/opt/lib TARGET=linux-glibc If the library we want to use is not installed on a unix system, then a locally installed library can be used (say, which is compiled and installed in the user home directory). In this case instead of /opt/include and /opt/lib the equivalent paths to the local installation should be specified. Of course, in that case the pkg-config tool can also be used if we have a complete installation (with .pc files). last but not least, if the pkg-config tool is not used when compiling, then HAProxy executable may not be able to find the OpenTracing C Wrapper library at startup. This can be solved in several ways, for example using the LD_LIBRARY_PATH environment variable which should be set to the path where the library is located before starting the HAProxy. % LD_LIBRARY_PATH=/opt/lib /path-to/haproxy ... Another way is to add RUNPATH to HAProxy executable that contains the path to the library in question. % make USE_OT=1 OT_RUNPATH=1 OT_INC=/opt/include OT_LIB=/opt/lib TARGET=linux-glibc After HAProxy is compiled, we can check if the OT filter is enabled: % ./haproxy -vv | grep opentracing --- command output ---------- [ OT] opentracing --- command output ---------- 3. Basic concepts in OpenTracing --------------------------------- Basic concepts of OpenTracing can be read on the OpenTracing documentation website https://opentracing.io/docs/overview/. Here we will list only the most important elements of distributed tracing and these are 'trace', 'span' and 'span context'. Trace is a description of the complete transaction we want to record in the tracing system. A span is an operation that represents a unit of work that is recorded in a tracing system. Span context is a group of information related to a particular span that is passed on to the system (from service to service). Using this context, we can add new spans to already open trace (or supplement data in already open spans). An individual span may contain one or more tags, logs and baggage items. The tag is a key-value element that is valid for the entire span. Log is a key-value element that allows you to write some data at a certain time, it can be used for debugging. A baggage item is a key-value data pair that can be used for the duration of an entire trace, from the moment it is added to the span. 4. OT configuration -------------------- In order for the OT filter to be used, it must be included in the HAProxy configuration, in the proxy section (frontend / listen / backend): frontend ot-test ... filter opentracing [id ] config ... If no filter id is specified, 'ot-filter' is used as default. The 'config' parameter must be specified and it contains the path of the file used to configure the OT filter. 4.1 OT scope ------------- If the filter id is defined for the OT filter, then the OT scope with the same name should be defined in the configuration file. In the same configuration file we can have several defined OT scopes. Each OT scope must have a defined (only one) "ot-tracer" section that is used to configure the operation of the OT filter and define the used groups and scopes. OT scope starts with the id of the filter specified in square brackets and ends with the end of the file or when a new OT scope is defined. For example, this defines two OT scopes in the same configuration file: [my-first-ot-filter] ot-tracer tracer1 ... ot-group group1 ... ot-scope scope1 ... [my-second-ot-filter] ... 4.2. "ot-tracer" section ------------------------- Only one "ot-tracer" section must be defined for each OT scope. There are several keywords that must be defined for the OT filter to work. These are 'config' which defines the configuration file for the OpenTracing API, and 'plugin' which defines the OpenTracing plugin used. Through optional keywords can be defined ACLs, logging, rate limit, and groups and scopes that define the tracing model. ot-tracer A new OT with the name is created. Arguments : name - the name of the tracer section The following keywords are supported in this section: - mandatory keywords: - config - plugin - optional keywords: - acl - debug-level - groups - [no] log - [no] option disabled - [no] option dontlog-normal - [no] option hard-errors - rate-limit - scopes acl [flags] [operator] ... Declare or complete an access list. To configure and use the ACL, see section 7 of the HAProxy Configuration Manual. config 'config' is one of the two mandatory keywords associated with the OT tracer configuration. This keyword sets the path of the configuration file for the OpenTracing tracer plugin. To set the contents of this configuration file, it is best to look at the documentation related to the OpenTracing tracer we want to use. Arguments : file - the path of the configuration file debug-level This keyword sets the value of the debug level related to the display of debug messages in the OT filter. The 'debug-level' value is binary, ie a single value bit enables or disables the display of the corresponding debug message that uses that bit. The default value is set via the FLT_OT_DEBUG_LEVEL macro in the include/config.h file. Debug level value is used only if the OT filter is compiled with the debug mode enabled, otherwise it is ignored. Arguments : value - binary value ranging from 0 to 255 (8 bits) groups ... A list of "ot-group" groups used for the currently defined tracer is declared. Several groups can be specified in one line. Arguments : name - the name of the OT group log global log [len ] [format ] [ []] no log Enable per-instance logging of events and traffic. To configure and use the logging system, see section 4.2 of the HAProxy Configuration Manual. option disabled no option disabled Keyword which turns the operation of the OT filter on or off. By default the filter is on. option dontlog-normal no option dontlog-normal Enable or disable logging of normal, successful processing. By default, this option is disabled. For this option to be considered, logging must be turned on. See also: 'log' keyword description. option hard-errors no option hard-errors During the operation of the filter, some errors may occur, caused by incorrect configuration of the tracer or some error related to the operation of HAProxy. By default, such an error will not interrupt the filter operation for the stream in which the error occurred. If the 'hard-error' option is enabled, the operation error prohibits all further processing of events and groups in the stream in which the error occurred. plugin 'plugin' is one of the two mandatory keywords associated with the OT tracer configuration. This keyword sets the path of the OpenTracing tracer plugin. Arguments : file - the name of the plugin used rate-limit This option allows limiting the use of the OT filter, ie it can be influenced whether the OT filter is activated for a stream or not. Determining whether or not a filter is activated depends on the value of this option that is compared to a randomly selected value when attaching the filter to the stream. By default, the value of this option is set to 100.0, ie the OT filter is activated for each stream. Arguments : value - floating point value ranging from 0.0 to 100.0 scopes ... This keyword declares a list of "ot-scope" definitions used for the currently defined tracer. Multiple scopes can be specified in the same line. Arguments : name - the name of the OT scope 4.3. "ot-scope" section ------------------------ Stream processing begins with filter attachment, then continues with the processing of a number of defined events and groups, and ends with filter detachment. The "ot-scope" section is used to define actions related to individual events. However, this section may be part of a group, so the event does not have to be part of the definition. ot-scope Creates a new OT scope definition named . Arguments : name - the name of the OT scope The following keywords are supported in this section: - acl - baggage - event - extract - finish - inject - log - span - tag acl [flags] [operator] ... Declare or complete an access list. To configure and use the ACL, see section 7 of the HAProxy Configuration Manual. baggage ... Baggage items allow the propagation of data between spans, ie allow the assignment of metadata that is propagated to future children spans. This data is formatted in the style of key-value pairs and is part of the context that can be transferred between processes that are part of a server architecture. This kewyord allows setting the baggage for the currently active span. The data type is always a string, ie any sample type is converted to a string. The exception is a binary value that is not supported by the OT filter. See the 'tag' keyword description for the data type conversion table. Arguments : name - key part of a data pair sample - sample expression (value part of a data pair), at least one sample must be present event [{ if | unless } ] Set the event that triggers the 'ot-scope' to which it is assigned. Optionally, it can be followed by an ACL-based condition, in which case it will only be evaluated if the condition is true. ACL-based conditions are executed in the context of a stream that processes the client and server connections. To configure and use the ACL, see section 7 of the HAProxy Configuration Manual. Arguments : name - the event name condition - a standard ACL-based condition Supported events are (the table gives the names of the events in the OT filter and the corresponding equivalent in the SPOE filter): -------------------------------------|------------------------------ the OT filter | the SPOE filter -------------------------------------|------------------------------ on-client-session-start | on-client-session on-frontend-tcp-request | on-frontend-tcp-request on-http-wait-request | - on-http-body-request | - on-frontend-http-request | on-frontend-http-request on-switching-rules-request | - on-backend-tcp-request | on-backend-tcp-request on-backend-http-request | on-backend-http-request on-process-server-rules-request | - on-http-process-request | - on-tcp-rdp-cookie-request | - on-process-sticking-rules-request | - on-client-session-end | - on-server-unavailable | - -------------------------------------|------------------------------ on-server-session-start | on-server-session on-tcp-response | on-tcp-response on-http-wait-response | - on-process-store-rules-response | - on-http-response | on-http-response on-server-session-end | - -------------------------------------|------------------------------ extract [use-vars | use-headers] For a more detailed description of the propagation process of the span context, see the description of the keyword 'inject'. Only the process of extracting data from the carrier is described here. Arguments : name-prefix - data name prefix (ie key element prefix) use-vars - data is extracted from HAProxy variables use-headers - data is extracted from the HTTP header Below is an example of using HAProxy variables to transfer span context data: --- test/ctx/ot.cfg -------------------------------------------------------- ... ot-scope client_session_start_2 extract "ot_ctx_1" use-vars span "Client session" child-of "ot_ctx_1" ... ---------------------------------------------------------------------------- finish ... Closing a particular span or span context. Instead of the name of the span, there are several specially predefined names with which we can finish certain groups of spans. So it can be used as the name '*req*' for all open spans related to the request channel, '*res*' for all open spans related to the response channel and '*' for all open spans regardless of which channel they are related to. Several spans and/or span contexts can be specified in one line. Arguments : name - the name of the span or context context inject [use-vars] [use-headers] In OpenTracing, the transfer of data related to the tracing process between microservices that are part of a larger service is done through the propagation of the span context. The basic operations that allow us to access and transfer this data are 'inject' and 'extract'. 'inject' allows us to extract span context so that the obtained data can be forwarded to another process (microservice) via the selected carrier. 'inject' in the name actually means inject data into carrier. Carrier is an interface here (ie a data structure) that allows us to transfer tracing state from one process to another. Data transfer can take place via one of two selected storage methods, the first is by adding data to the HTTP header and the second is by using HAProxy variables. Only data transfer via HTTP header can be used to transfer data to another process (ie microservice). All data is organized in the form of key-value data pairs. No matter which data transfer method you use, we need to specify a prefix for the key element. All alphanumerics (lowercase only) and underline character can be used to construct the data name prefix. Uppercase letters can actually be used, but they will be converted to lowercase when creating the prefix. Arguments : name-prefix - data name prefix (ie key element prefix) use-vars - HAProxy variables are used to store and transfer data use-headers - HTTP headers are used to store and transfer data Below is an example of using HTTP headers and variables, and how this is reflected in the internal data of the HAProxy process. --- test/ctx/ot.cfg -------------------------------------------------------- ... ot-scope client_session_start_1 span "HAProxy session" root inject "ot_ctx_1" use-headers use-vars ... ---------------------------------------------------------------------------- - generated HAProxy variable (key -> value): txn.ot_ctx_1.uberDtraceDid -> 8f1a05a3518d2283:8f1a05a3518d2283:0:1 - generated HTTP header (key: value): ot_ctx_1-uber-trace-id: 8f1a05a3518d2283:8f1a05a3518d2283:0:1 Because HAProxy does not allow the '-' character in the variable name (which is automatically generated by the OpenTracing API and on which we have no influence), it is converted to the letter 'D'. We can see that there is no such conversion in the name of the HTTP header because the '-' sign is allowed there. Due to this conversion, initially all uppercase letters are converted to lowercase because otherwise we would not be able to distinguish whether the disputed sign '-' is used or not. Thus created HTTP headers and variables are deleted when executing the 'finish' keyword or when detaching the stream from the filter. log ... This kewyord allows setting the log for the currently active span. The data type is always a string, ie any sample type is converted to a string. The exception is a binary value that is not supported by the OT filter. See the 'tag' keyword description for the data type conversion table. Arguments : name - key part of a data pair sample - sample expression (value part of a data pair), at least one sample must be present span [] Creating a new span (or referencing an already opened one). If a new span is created, it can be a child of the referenced span, follow from the referenced span, or be root 'span'. In case we did not specify a reference to the previously created span, the new span will become the root span. We need to pay attention to the fact that in one trace there can be only one root span. In case we have specified a non-existent span as a reference, a new span will not be created. Arguments : name - the name of the span being created or referenced (operation name) reference - span or span context to which the created span is referenced tag ... This kewyord allows setting a tag for the currently active span. The first argument is the name of the tag (tag ID) and the second its value. A value can consist of one or more data. If the value is only one data, then the type of that data depends on the type of the HAProxy sample. If the value contains more data, then the data type is string. The data conversion table is below: HAProxy sample data type | the OpenTracing data type --------------------------+--------------------------- NULL | NULL BOOL | BOOL INT32 | INT64 UINT32 | UINT64 INT64 | INT64 UINT64 | UINT64 IPV4 | STRING IPV6 | STRING STRING | STRING BINARY | UNSUPPORTED --------------------------+--------------------------- Arguments : name - key part of a data pair sample - sample expression (value part of a data pair), at least one sample must be present 4.4. "ot-group" section ------------------------ This section allows us to define a group of OT scopes, that is not activated via an event but is triggered from TCP or HTTP rules. More precisely, these are the following rules: 'tcp-request', 'tcp-response', 'http-request', 'http-response' and 'http-after-response'. These rules can be defined in the HAProxy configuration file. ot-group Creates a new OT group definition named . Arguments : name - the name of the OT group The following keywords are supported in this section: - scopes scopes ... 'ot-scope' sections that are part of the specified group are defined. If the mentioned 'ot-scope' sections are used only in some OT group, they do not have to have defined events. Several 'ot-scope' sections can be specified in one line. Arguments : name - the name of the 'ot-scope' section 5. Examples ------------ Several examples of the OT filter configuration can be found in the test directory. A brief description of the prepared configurations follows: cmp - the configuration very similar to that of the spoa-opentracing project. It was made to compare the speed of the OT filter with the implementation of distributed tracing via spoa-opentracing application. sa - the configuration in which all possible events are used. ctx - the configuration is very similar to the previous one, with the only difference that the spans are opened using the span context as a span reference. fe be - a slightly more complicated example of the OT filter configuration that uses two cascaded HAProxy services. The span context between HAProxy processes is transmitted via the HTTP header. empty - the empty configuration in which the OT filter is initialized but no event is triggered. It is not very usable, except to check the behavior of the OT filter in the case of a similar configuration. In order to be able to collect data (and view results via the web interface) we need to install some of the supported tracers. We will use the Jaeger tracer as an example. Installation instructions can be found on the website https://www.jaegertracing.io/download/. For the impatient, here we will list how the image to test the operation of the tracer system can be installed without much reading of the documentation. # docker pull jaegertracing/all-in-one:latest # docker run -d --name jaeger -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \ -p 5775:5775/udp -p 6831:6831/udp -p 6832:6832/udp -p 5778:5778 \ -p 16686:16686 -p 14268:14268 -p 9411:9411 jaegertracing/all-in-one:latest The last command will also initialize and run the Jaeger container. If we want to use that container later, it can be started and stopped in the classic way, using the 'docker container start/stop' commands. In order to be able to use any of the configurations from the test directory, we must also have a tracer plugin in that directory (all examples use the Jaeger tracer plugin). The simplest way is to download the tracer plugin using the already prepared shell script get-opentracing-plugins.sh. The script accepts one argument, the directory in which the download is made. If run without an argument, the script downloads all plugins to the current directory. % ./get-opentracing-plugins.sh After that, we can run one of the pre-configured configurations using the provided script run-xxx.sh (where xxx is the name of the configuration being tested). For example: % ./run-sa.sh The script will create a new log file each time it is run (because part of the log file name is the start time of the script). Eh, someone will surely notice that all test configurations use the Jaeger tracing plugin that cannot be downloaded using the get-opentracing-plugins.sh script. Unfortunately, the latest precompiled version that can be downloaded is 0.4.2, for newer ones only the source code can be found. Version 0.4.2 has a bug that can cause the operation of the OT filter to get stuck, so it is better not to use this version. Here is the procedure by which we can compile a newer version of the plugin (in our example it is 0.5.0). Important note: the GCC version must be at least 4.9 or later. % wget https://github.com/jaegertracing/jaeger-client-cpp/archive/v0.5.0.tar.gz % tar xf v0.5.0.tar.gz % cd jaeger-client-cpp-0.5.0 % mkdir build % cd build % cmake -DCMAKE_INSTALL_PREFIX=/opt -DJAEGERTRACING_PLUGIN=ON -DHUNTER_CONFIGURATION_TYPES=Release -DHUNTER_BUILD_SHARED_LIBS=OFF .. % make After the plugin is compiled, it will be in the current directory. The name of the plugin is libjaegertracing_plugin.so. 5.1. Benchmarking results -------------------------- To check the operation of the OT filter, several different test configurations have been made which are located in the test directory. The test results of the same configurations (with the names README-speed-xxx, where xxx is the name of the configuration being tested) are also in the directory of the same name. All tests were performed on the same debian 9.13 system, CPU i7-4770, 32 GB RAM. For the purpose of testing, the thttpd web server on port 8000 was used. Testing was done with the wrk utility running via run-xxx.sh scripts; that is, via the test-speed.sh script that is run as follows: % ./test-speed.sh all The above mentioned thttpd web server is run from that script and it should be noted that we need to have the same installed on the system (or change the path to the thttpd server in that script if it is installed elsewhere). Each test is performed several times over a period of 5 minutes per individual test. The only difference when running the tests for the same configuration was in changing the 'rate-limit' parameter (and the 'option disabled' option), which is set to the following values: 100.0, 50.0, 10.0, 2.5 and 0.0 percent. Then a test is performed with the OT filter active but disabled for request processing ('option disabled' is included in the ot.cfg configuration). In the last test, the OT filter is not used at all, ie it is not active and does not affect the operation of HAProxy in any way. 6. OT CLI ---------- Via the HAProxy CLI interface we can find out the current status of the OT filter and change several of its settings. All supported CLI commands can be found in the following way, using the socat utility with the assumption that the HAProxy CLI socket path is set to /tmp/haproxy.sock (of course, instead of socat, nc or other utility can be used with a change in arguments when running the same): % echo "help" | socat - UNIX-CONNECT:/tmp/haproxy.sock | grep flt-ot --- command output ---------- flt-ot debug [level] : set the OT filter debug level (default: get current debug level) flt-ot disable : disable the OT filter flt-ot enable : enable the OT filter flt-ot soft-errors : turning off hard-errors mode flt-ot hard-errors : enabling hard-errors mode flt-ot logging [state] : set logging state (default: get current logging state) flt-ot rate [value] : set the rate limit (default: get current rate value) flt-ot status : show the OT filter status --- command output ---------- 'flt-ot debug' can only be used in case the OT filter is compiled with the debug mode enabled. 7. Known bugs and limitations ------------------------------ The name of the span context definition can contain only letters, numbers and characters '_' and '-'. Also, all uppercase letters in the name are converted to lowercase. The character '-' is converted internally to the 'D' character, and since a HAProxy variable is generated from that name, this should be taken into account if we want to use it somewhere in the HAProxy configuration. The above mentioned span context is used in the 'inject' and 'extract' keywords. Let's look a little at the example test/fe-be (configurations are in the test/fe and test/be directories, 'fe' is here the abbreviation for frontend and 'be' for backend). In case we have the 'rate-limit' set to a value less than 100.0, then distributed tracing will not be started with each new HTTP request. It also means that the span context will not be delivered (via the HTTP header) to the backend HAProxy process. The 'rate-limit' on the backend HAProxy must be set to 100.0, but because the frontend HAProxy does not send a span context every time, all such cases will cause an error to be reported on the backend server. Therefore, the 'hard-errors' option must be set on the backend server, so that processing on that stream is stopped as soon as the first error occurs. Such cases will slow down the backend server's response a bit (in the example in question it is about 3%).