├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── doc └── overview.edoc ├── docs └── images │ ├── otter_flow.png │ └── otter_logo.png ├── elvis.config ├── eqc ├── ol_eqc.erl └── thrift_eqc.erl ├── include └── otters.hrl ├── priv ├── otters.schema └── zipkinCore.thrift ├── rebar.config ├── rebar.lock ├── rebar3 ├── src ├── of_lexer.xrl ├── of_parser.yrl ├── ol.erl ├── ol_filter.erl ├── otters.app.src ├── otters.erl ├── otters_app.erl ├── otters_config.erl ├── otters_conn_zipkin.erl ├── otters_lib.erl ├── otters_snapshot_count.erl ├── otters_sup.erl ├── otters_zipkin_encoder.erl └── ottersp.erl └── test ├── bench_SUITE.erl ├── otter_SUITE.erl ├── otters_filter.erl ├── test_httpc.config └── test_ibrowse.config /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | *.o 3 | *.beam 4 | *.plt 5 | erl_crash.dump 6 | ebin 7 | rel/example_project 8 | .concrete/DEV_MODE 9 | .rebar 10 | _build/ 11 | *~ 12 | doc 13 | src/of_lexer.erl 14 | src/of_parser.erl 15 | .DS_Store 16 | .eqc-info 17 | .eqc/ 18 | .rebar3/ 19 | current_counterexample.eqc 20 | rebar3.crashdump 21 | \#* 22 | .#* 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | 3 | before_script: 4 | - kerl list installations 5 | 6 | otp_release: 7 | - 18.1 8 | - 18.2 9 | - 18.3 10 | - 19.0 11 | - 19.1 12 | - 19.2 13 | 14 | sudo: false 15 | install: true 16 | script: 17 | - ./rebar3 xref 18 | - ./rebar3 dialyzer 19 | - ./rebar3 ct --suite test/otter_SUITE 20 | 21 | branches: 22 | only: 23 | - master 24 | - dev 25 | - test 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all compile clean ct upgrade shell distclean 2 | 3 | REBAR=rebar3 4 | 5 | all: compile 6 | 7 | compile: 8 | @${REBAR} compile 9 | 10 | shell: 11 | @${REBAR} shell 12 | 13 | ct: 14 | @${REBAR} ct --sys_config test/test_httpc.config 15 | @${REBAR} ct --sys_config test/test_ibrowse.config 16 | 17 | test: ct 18 | 19 | clean: 20 | @${REBAR} clean 21 | 22 | upgrade: 23 | @${REBAR} upgrade 24 | 25 | distclean: clean 26 | @rm -rf ./_build/ && rm -rf rebar.lock 27 | 28 | rebar3: 29 | wget https://s3.amazonaws.com/rebar3/rebar3 && chmod +x rebar3 30 | 31 | dialyzer: 32 | @${REBAR} dialyzer 33 | 34 | docs: 35 | @${REBAR} edoc 36 | 37 | xref: 38 | ${REBAR} xref 39 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | otter - Open Tracing Tool for ERlang 2 | Copyright 2017 Bluehouse Technology Ltd. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/project-fifo/otters.svg)](https://travis-ci.org/project-fifo/otters) [![Hex pm](http://img.shields.io/hexpm/v/otters.svg?style=flat)](https://hex.pm/packages/otters) [Docs](https://hexdocs.pm/otters) 2 | 3 | 4 | # OTTERS 5 | 6 | Cleanup of otter with a community focus. Because multiple otters build a damn a lot faster! 7 | 8 | Otters is 9 | 10 | * **up to 10x faster** when filtering spans 11 | * **up to 20x faster** encoding data to send it 12 | * using an easy to use filtering language 13 | * including less invasive dependencies 14 | * Dialyzer 'clean' 15 | * Proactive im merging community contributes 16 | * using EQC tests for higher test coverage 17 | * using Elvis listing for code style 18 | * providing a cleaner API 19 | * more riddled with typos in the docs 20 | 21 | OpenTracing Toolkit for Erlang 22 | 23 | ![otter logo](docs/images/otter_logo.png) 24 | 25 | ## Performance 26 | 27 | Instrumentation should have minimal impact on production code, so otters is tuned to be fast. So a few back of the envelop numbers (YMMV): 28 | 29 | - filtering a span (using the filter language): < 1 microsecond (~3-10x improvement) 30 | - encoding 1 spawn to be send: < 10 microseconds (~400x improvement) 31 | 32 | 33 | ## Build 34 | 35 | OTTER uses [rebar3](http://www.rebar3.org) as build tool. It can be built 36 | with: 37 | 38 | ``` 39 | rebar3 compile 40 | ``` 41 | 42 | However most likely you'll want to add it to your project in your build 43 | environment. 44 | 45 | ## Dependencies 46 | 47 | [ibrowse](https://github.com/cmullaparthi/ibrowse) HTTP client is used 48 | to send the HTTP/Thrift requests to Zipkin. 49 | 50 | ## OpenTracing 51 | 52 | [OpenTracing](http://opentracing.io) is an open initiative to provide a 53 | set of terms and methods to produce, collect and correlate trace 54 | information in a distributed environment across different programming 55 | languages, platforms and protocols. 56 | 57 | The concept defined for trace production is based on a **span** which is 58 | essentially a record of a handling in one environment. A **span** has a 59 | **timestamp** of when it started, a **duration**, a list of timestamped 60 | events marking the timing of important actions during the **span** and a 61 | list of key-value tags storing the parameters of the handled request 62 | (e.g. customer ids, transaction ids, results of subsequent actions). 63 | The **span** also contains id's to aid their correlation. The 64 | **trace_id** is used for correlating **span**s related to the handling 65 | of one request received from different systems. The **trace_id** is 66 | generated in the first system which starts handling a request (e.g. a 67 | frontend) and supposed to be passed on to other systems involved in the 68 | processing the same request. This is fairly simple when the protocols 69 | are fully under control and extensible (e.g. HTTP). 70 | Other id's recorded are the **span_id** and a **parent_id** referring to 71 | the parent **span** to help showing a hierarchical relationship of 72 | the **span**s in the **trace collector**. 73 | 74 | After collecting this information, the **span** can be sent to a trace 75 | collector, which based on the id's of the received spans can 76 | correlate them and provide and end-to-end view of the request. 77 | Sending all produced **span**s could generate significant additional 78 | load on the system that produces them and also on the **trace collector**. 79 | It is recommended to filter the the **span**s before sending them to the 80 | collector. 81 | 82 | The most mature **trace collector** at the time of the initial development 83 | is [OpenZipkin](http://zipkin.io). OTTER provides an interface to send 84 | spans to Zipkin using the HTTP/Thrift binary protocol. 85 | 86 | The OpenTracing terminology defines information to be passed on across 87 | systems. The feasibility of this in most cases depends on the protocols 88 | used, and sometimes rather difficult to achieve. OTTER is not attempting to 89 | implement any of this functionality. It is possible though to initialize 90 | a **span** in OTTER with a **trace_id** and **parent_id**, but how these 91 | id's are passed across the systems is left to the particular implementation. 92 | 93 | ## OTTER functionality 94 | 95 | OTTER helps producing span information, filtering spans, sending to 96 | trace collector (Zipkin) and also counting and keeping a snapshot of the last 97 | occurrence of a span. 98 | 99 | 100 | ![otter flow](docs/images/otter_flow.png) 101 | 102 | 103 | ### Producing span information 104 | 105 | The main motivation behind the span collection of Otter is to make the 106 | instrumentation of existing code as simple as possible. 107 | 108 | #### Types used in the API 109 | 110 | The following type specifications are in otter.hrl 111 | 112 | ```erlang 113 | -type time_us() :: integer(). % timestamp in microseconds 114 | -type info() :: binary() | iolist() | atom() | integer(). 115 | -type ip4() :: {integer(), integer(), integer(), integer()}. 116 | -type service() :: binary() | list() | default | {binary() | list(), ip4(), integer()}. 117 | -type trace_id():: integer(). 118 | -type span_id() :: integer(). 119 | 120 | -record(span , { 121 | timestamp :: time_us(), % timestamp of starting the span 122 | trace_id :: trace_id(), % 64 bit integer trace id 123 | name :: info(), % name of the span 124 | id :: span_id(), % 64 bit integer span id 125 | parent_id :: span_id() | undefined, % 64 bit integer parent span id 126 | tags = [] :: [{info(), info()} | {info(), info(), service()}], % span tags 127 | logs = [] :: [{time_us(), info()} | {info(), info(), service()}], % span logs 128 | duration :: time_us() % microseconds between span start/end 129 | }). 130 | 131 | -type span() :: #span{}. 132 | -type maybe_span() :: span() | undefined. 133 | 134 | 135 | ``` 136 | 137 | #### Functional API (`otters:`) 138 | 139 | Passing the span structure between requests, this API will probably 140 | require to pass the span in function calls if the function has 141 | something to add to the span. This requires more code changes, however 142 | when functions pass non-strict composite structures (e.g. maps or 143 | proplists) then inserting the span information is more or less trivial. 144 | 145 | Start span with name only. Name should refer e.g. to the interface. 146 | ```erlang 147 | -spec start(Name::info()) -> span(). 148 | ``` 149 | 150 | Start span with name and trace_id where trace_id e.g. received from 151 | protocol. 152 | 153 | ```erlang 154 | -spec start(Name::info(), TraceId::integer() | span()) -> span(). 155 | ``` 156 | 157 | Start span with name, trace_id and parent span id e.g. received from 158 | protocol. 159 | 160 | ```erlang 161 | -spec start(Name::info(), TraceId::integer(), ParentId::integer()) -> span(). 162 | ``` 163 | 164 | Start span with name and parent span. trace_id and parent span id are 165 | extracted from the parent. 166 | 167 | ```erlang 168 | -spec start(info(), ParentSpan :: span()) -> span(). 169 | ``` 170 | 171 | Add a tag to the previously started span. 172 | ```erlang 173 | -spec tag(Span::maybe_span(), Key::info(), Value::info()) -> maybe_span(). 174 | ``` 175 | 176 | Add a tag to the previously started span with additional service information. 177 | ```erlang 178 | -spec tag(Span::maybe_span(), Key::info(), Value::info(), Service::service()) -> maybe_span(). 179 | ``` 180 | 181 | Add a log/event to the previously started span 182 | ```erlang 183 | -spec log(Span::maybe_span(), Text::info()) -> maybe_span(). 184 | ``` 185 | 186 | Add a log/event to the previously started span with additional service information0 187 | ```erlang 188 | -spec log(Span::maybe_span(), Text::info(), Service::service()) -> maybe_span(). 189 | ``` 190 | 191 | End span and invoke the span filter (see below) 192 | ```erlang 193 | -spec finish(Span::maybe_span()) -> ok. 194 | ``` 195 | 196 | Get span id's. Return the **trace_id** and the **span** id from the 197 | currently started span. This can be used e.g. when process "boundary" is 198 | to be passed and eventually new span needs this information. Also when 199 | these id's should be passed to a protocol interface for another system 200 | ```erlang 201 | -spec ids(maybe_span()) -> {trace_id(), span_id()} | undefined. 202 | ``` 203 | example : 204 | 205 | ```erlang 206 | ... 207 | Span = otters:start("radius request"), 208 | ... 209 | ... 210 | Span1 = otters:tag(Span, "request_id", RequestId), 211 | ... 212 | ... 213 | Span2 = otters:log(Span1, "invoke user db"), 214 | ... 215 | ... 216 | Span3 = otters:log(Span2, "user db result"), 217 | Span4 = otters:tag(Span3, "user db result", "ok"), 218 | ... 219 | ... 220 | Span5 = otters:tag(Span4, "final result", "error"), 221 | Span6 = otters:tag(Span5, "final result reason", "unknown user"), 222 | otters:end(Span6), 223 | ... 224 | ``` 225 | 226 | #### Process API (`ottersp:`) 227 | 228 | The simplest API uses the process dictionary to store span information. 229 | This is probably the least work to implement in existing code. 230 | 231 | Start span with name only. Name should refer e.g. to the interface. 232 | 233 | ```erlang 234 | -spec start(Name::info()) -> ok. 235 | ``` 236 | 237 | Start span with name and trace_id where trace_id e.g. received from 238 | protocol. 239 | 240 | ```erlang 241 | -spec start(Name::info(), TraceId::trace_id() | span()) -> ok. 242 | ``` 243 | 244 | Start span with name, trace_id and parent span id e.g. received from 245 | protocol. 246 | 247 | ```erlang 248 | -spec start(Name::info(), TraceId::trace_id(), ParnetId::span_id()) -> ok. 249 | ``` 250 | 251 | Add a tag to the previously started span. 252 | 253 | ```erlang 254 | -spec tag(Key::info(), Value::info()) -> ok. 255 | ``` 256 | 257 | Add a tag to the previously started span with additional service information 258 | 259 | ```erlang 260 | -spec tag(Key::info(), Value::info(), Service::service()) -> ok. 261 | ``` 262 | 263 | Add a log/event to the previously started span 264 | ```erlang 265 | -spec log(Text::info()) -> ok. 266 | ``` 267 | 268 | Add a log/event to the previously started span with additional service information 269 | ```erlang 270 | -spec log(Text::info(), Service::service()) -> ok. 271 | ``` 272 | 273 | 274 | End span and invoke the span filter (see below) 275 | ```erlang 276 | -spec finish() -> ok. 277 | ``` 278 | 279 | Get span id's. Return the **trace_id** and the **span** id from the 280 | currently started span. This can be used e.g. when process "boundary" is 281 | to be passed and eventually new span needs this information. Also when 282 | these id's should be passed to a protocol interface for another system 283 | 284 | ```erlang 285 | -spec ids() -> {trace_id(), span_id()} | undefined. 286 | ``` 287 | 288 | Return the current span. e.g. it can be handed to another process to 289 | continue collecting span information using the functional API. 290 | 291 | ``` 292 | -spec span_get() -> maybe_span(). 293 | ``` 294 | 295 | example : 296 | 297 | ```erlang 298 | ... 299 | ottersp:start("radius request"), 300 | ... 301 | ... 302 | ottersp:tag("request_id", RequestId), 303 | ... 304 | ... 305 | ottersp:log("invoke user db"), 306 | ... 307 | ... 308 | ottersp:log("user db result"), 309 | ottersp:tag("user_db_result", "ok"), 310 | ... 311 | ... 312 | ottersp:tag("final_result", "error"), 313 | ottersp:tag("final_result_reason", "unknown user"), 314 | ottersp:end(), 315 | ... 316 | ``` 317 | 318 | #### tag/log information 319 | 320 | A note on the tag key/value and log types: the Zipkin interface requires 321 | string types. The Zipkin connector module (otter_conn_zipkin.erl) attempts 322 | to convert: integer, atom, and iolist types to binary. Unknown data types 323 | (e.g. record, tuples, or maps) are converted using the "~p" io:fwrite formating 324 | control character. The resulting string might be hard to read for non-Erlang 325 | people, but it is still better than loosing the information completely. 326 | 327 | If the generation of log values is complex or computational expensive, a 328 | arity zero fun can be passed as info. The function is executed in the 329 | connector module and thereby after span_end has been called. 330 | 331 | Adding service information to tags and logs means that otter adds a host 332 | structure to each of these elements. The extra optional service parameter 333 | in the relevant API calls can have 3 formats. 334 | 335 | The atom ```default``` will include service/host information based on 336 | the following configuration parameters of the zipkin connector. 337 | 338 | ```erlang 339 | ... 340 | {zipkin_tag_host_ip, {127,0,0,1}}, 341 | {zipkin_tag_host_port, 0}, 342 | {zipkin_tag_host_service, "otter_test"}, 343 | ... 344 | ``` 345 | 346 | Name of the service as a string in binary() or list() format also adds 347 | the host information based on the configuration above, except the service 348 | name will be as specified in the parameter. 349 | 350 | A 3 element tuple ```{Service, Ip, Port}``` which will be used to compose 351 | the information. This can be interesting to compose "ca" and "sa" opentracing 352 | tags where the host information may refer to a remote server/client node 353 | instead of the one where the span is generated. 354 | 355 | #### Configuration 356 | 357 | There is no configuration involved in the stage of producing span data. 358 | The paramers mentioned above are functionally specific to the zipkin 359 | connector. It was simpler to explain them though in this context. 360 | 361 | Please also consult the cuttle fisch schema file `priv/otters.schema` for valid configuration options. 362 | 363 | ### Span Filtering 364 | 365 | The initial filter file can be configured with the `filter_file` app config value. 366 | 367 | When the collection of **span** information is completed (i.e. `finish` is called), filtering is invoked. Filtering is based on the 368 | tags collected in the span with the **span name** and the 369 | **span duration** added to the key/value pair list with keys : 370 | **otter_span_name** and **otter_span_duration**. The resulting 371 | key/value pair list which is used as input of the filter rules. With the 372 | examples above it can look like this : 373 | 374 | ```erlang 375 | [ 376 | {<<"otter_span_name">>, "radius request"}, 377 | {<<"otter_span_duration">>, 1202}, 378 | {<<"request_id">>, "6390266399200312"}, 379 | {<<"user_db_result">>, "ok"}, 380 | {<<"final_result">>, "error"}, 381 | {<<"final_result_reason">>, "unknown user"} 382 | ] 383 | ``` 384 | 385 | This key/value pair list is passed to a sequence of conditions/actions 386 | pairs. In each pair the, conditions are a list of checks against the 387 | key/value pair list. If all conditions in the list are true, the actions 388 | are executed. An empty condition list always returns a positive match. 389 | 390 | 391 | #### Filtering language 392 | 393 | Otters uses a filter language that is dynamically compiled to a erlang module for performance. The Module is called `ol_filter`. 394 | 395 | The filter language is loosely based on Erlang syntax and functions a bit like firewall rules. 396 | 397 | A multiple filter rules can be used, and each filter can have multiple conditions on which actions are taken. 398 | 399 | The general syntax is: 400 | 401 | ```erlang 402 | ([]) -> 403 | . 404 | ``` 405 | 406 | ##### Conditions 407 | 408 | Basic conditions are: 409 | 410 | - none : (as in `()`) always matches. 411 | - ` `: where `comparison` is either `>`, `<`, `>=`, `=<`, `==` or `/=` 412 | - ``: weather or not a key is present 413 | 414 | ##### Actions 415 | 416 | Possible actions are: 417 | 418 | - `drop`: stops filtering for this and the following rules. 419 | - `skip`: skips the rest of this rule and continues with the next. 420 | - `send`: sends the span 421 | - `continue` : only continues the current otherwise skips 422 | - `count(, ...)`: where values can either be a string `'bla'` which is taken literally or a keyword `otter_span_duration` to lookup the value. 423 | 424 | ##### Examples 425 | 426 | Comparing the old style filters 427 | ```erlang 428 | [ 429 | { 430 | %% Condition 431 | [ 432 | {greater, "otter_span_duration", 5000000}, 433 | {value, "otter_span_name", "radius request"} 434 | ], 435 | %% Action 436 | [ 437 | {snap_count, [long_radius_request], []}, 438 | send_to_zipkin 439 | ] 440 | }, 441 | { 442 | %% Condition counts all requests with name and result 443 | [ 444 | ], 445 | %% Action 446 | [ 447 | {snap_count, [request], ["otter_span_name", "final_result"]} 448 | ] 449 | } 450 | 451 | ] 452 | ``` 453 | 454 | Translate to 455 | ```erlang 456 | slow_spans(otter_span_duration > 5000000) -> continue. 457 | %% Skip requests that are not radius requests 458 | slow_spans(otter_span_name == 'radius request') -> continue. 459 | %% Count 460 | slow_spans() -> count('long_radius_request'). 461 | %% Send 462 | slow_spans() -> send. 463 | 464 | %% Count them all 465 | count() -> count('request', otter_span_name, final_result). 466 | ``` 467 | 468 | #### Filter conditions (old style) 469 | 470 | ##### Check the presence of a Key 471 | 472 | 473 | ```erlang 474 | {present, Key} 475 | ``` 476 | 477 | ##### Check whether 2 Keys have the same value 478 | 479 | ```erlang 480 | {same, Key1, Key2} 481 | ``` 482 | 483 | ##### Compare a value 484 | 485 | The value of a Key/Value pair can be compared to a value 486 | 487 | ```erlang 488 | {value, Key, ValueToCompare} 489 | ``` 490 | 491 | example: check the name of the span 492 | 493 | ```erlang 494 | {value, otter_span_name, "radius request"} 495 | ``` 496 | 497 | ##### Checking integer values 498 | 499 | Key/Value pairs with integer values can be checked with the following 500 | conditions. 501 | 502 | ```erlang 503 | {greater, Key, Integer} 504 | 505 | {less, Key, Integer} 506 | 507 | {between, Key, Integer1, Integer2} 508 | ``` 509 | 510 | example: check whether the span duration is greater than 5 seconds 511 | 512 | ```erlang 513 | {greater, "otter_span_duration", 5000000} 514 | ``` 515 | 516 | ##### Negate condition check 517 | 518 | ```erlang 519 | {negate, Condition} 520 | ``` 521 | 522 | example: Check if the final result is other than ok 523 | 524 | ```erlang 525 | {negate, {value, "final_result", "ok"}} 526 | ``` 527 | 528 | #### Filter Actions 529 | 530 | ##### Snapshot/Count 531 | 532 | Snapshot/Count increases a counter with a key composed by a fixed prefix and 533 | values of Key/Values in the Key/Value list. The key is a list of parameters. 534 | Also it stores the last **span** that triggers the counter in an ets 535 | table. These cheap snapshots can be used for initial analysis of eventual 536 | problems. The snapshots and counter can be retrieved by the otter counter 537 | API (see below). 538 | 539 | ```erlang 540 | {snapshot_count, Prefix, KeyList} 541 | ``` 542 | 543 | example: snapshot/count any request that take long in different counters for 544 | each span name and final result. The condition example above with the 545 | otter_span_duration could be used to trigger this action. 546 | 547 | ```erlang 548 | {snapshot_count, [long_request], [otter_span_name, "final_result"]} 549 | ``` 550 | 551 | This will produce a counter and snapshot with e.g. such key : 552 | 553 | ```erlang 554 | [long_request, "radius request", "ok"] 555 | ``` 556 | 557 | ##### Send span to Zipkin 558 | 559 | This action triggers sending the span to Zipkin 560 | 561 | ```erlang 562 | send_to_zipkin 563 | ``` 564 | 565 | ##### Stop evaluating further Condition/Action pairs 566 | 567 | Normally each span triggers checking of all Condition/Action pairs in the 568 | sequence (executing all relevant actions). However if for a particular set 569 | of conditions it is not necessary, the break action can be used. When 570 | this is found in an Action list then all actions in the current list are 571 | executed and no further Condition/Action pairs are checked. 572 | 573 | ```erlang 574 | break 575 | ``` 576 | 577 | #### Filter configuration 578 | 579 | The filter rules are configured under **filter_rules** 580 | 581 | #### Example 582 | 583 | example Condition/Action (rule) list: 584 | 585 | ```erlang 586 | [ 587 | { 588 | %% Condition 589 | [ 590 | {greater, "otter_span_duration", 5000000}, 591 | {value, "otter_span_name", "radius request"} 592 | ], 593 | %% Action 594 | [ 595 | {snap_count, [long_radius_request], []}, 596 | send_to_zipkin 597 | ] 598 | }, 599 | { 600 | %% Condition counts all requests with name and result 601 | [ 602 | ], 603 | %% Action 604 | [ 605 | {snap_count, [request], ["otter_span_name", "final_result"]} 606 | ] 607 | } 608 | 609 | ] 610 | ``` 611 | 612 | ### Sending a span to Zipkin 613 | 614 | As a result of filter action **send_to_zipkin** the span is forwarded to 615 | the trace collector using HTTP/Thrift binary protocol. In the context of 616 | the span producing process the span is added to a buffer (ETS table). The 617 | content of this buffer is sent to Zipkin asynchronously in intervals 618 | configured in **zipkin_batch_interval_ms** (milliseconds). 619 | 620 | The URI of the Zipkin trace collector is configured in **zipkin_collector_uri**. 621 | 622 | Zipkin requires a node entry for every single tag collected in the span. 623 | This entry contains the service name and IP/Port of the node sending the 624 | span. If there is no tag/log is produced during span collecion with service/host 625 | paramer, the Zipkin connector module (otter_conn_zipkin.erl) can be configured 626 | to add an extra tag to each span during encoding the span by setting the 627 | **zipkin_add_host_tag_to_span** in the configuration. The value of the 628 | parameter should a tuple ```{Key, Value}``` which will be used as the 629 | default tag information. OpenZipkin uses the "lc" (Local Component) tag 630 | to display the service for a span. 631 | 632 | example : 633 | 634 | ```erlang 635 | {zipkin_add_host_tag_to_span, {"lc", ""}}, 636 | ``` 637 | 638 | The default service/host information to be sent to zipkin is provided in 639 | **zipkin_tag_host_service**, **zipkin_tag_host_ip** and **zipkin_tag_host_port** 640 | configuration parameters. 641 | 642 | It is also possible to add the default service/host information (as specified) 643 | in the configuration paramers above to each tag/log that does not have it 644 | explicitly set by setting the **zipkin_add_default_service_to_logs** and 645 | **zipkin_add_default_service_to_tags** to ```true```. This however can increase 646 | the amount of data to be sent to zipkin significantly (depending on the 647 | number of logs/tags) so probably it is not recommended for most uses. 648 | 649 | example : 650 | 651 | ```erlang 652 | ... 653 | {zipkin_add_default_service_to_logs, false}, 654 | {zipkin_add_default_service_to_tags, false}, 655 | ... 656 | ``` 657 | 658 | Sending the span to Zipkin utilizes the [ibrowse](https://github.com/cmullaparthi/ibrowse) 659 | http client (which is the only dependency of OTTER). 660 | 661 | ### Snapshot/Counter 662 | 663 | As a result of the filter snap_count action, 2 ETS tables are used to 664 | count events (see snap_count action above) and in the same time store 665 | the last span information that has increased the counter. This can be 666 | considered a useful and fairly cheap troubleshooting tool. 667 | 668 | Tho retrieve and manage these snapshot counters, OTTER provides API calls 669 | in the otter API module. 670 | 671 | ##### List counters 672 | 673 | ```erlang 674 | -spec counter_list() -> [{list(), integer()}]. 675 | ``` 676 | 677 | example: 678 | 679 | ```erlang 680 | 3> otter:counter_list(). 681 | [{[long_span,test_request],1}, 682 | {[otter_conn_zipkin,send_spans,failed],1}, 683 | {[span_processed,"customer db lookup"],1}, 684 | {[span_processed,test_request],1}, 685 | {[long_span,"customer db lookup"],1}] 686 | ``` 687 | 688 | ##### Retrieve snapshot for counter 689 | 690 | ```erlang 691 | -spec counter_snapshot(list()) -> term(). 692 | ``` 693 | 694 | example: 695 | 696 | ```erlang 697 | 4> otter:counter_snapshot([long_span,test_request]). 698 | [{[long_span,test_request], 699 | [{snap_timestamp,{2017,2,21,19,8,23,76525}}, 700 | {data,{span,1487700503067982,3826404163842487863, 701 | test_request,1113017739039451686,undefined, 702 | [{customer_id,1}, 703 | {transaction_id,3232323}, 704 | {magic_tag,"wizz"}, 705 | {magic_result,error}, 706 | {db_result,"bad customer"}, 707 | {final_result,error}], 708 | [{1487700503067998,"starting some magic"}, 709 | {1487700503068000,"finished magic"}, 710 | {1487700503068012,"db lookup"}, 711 | {1487700503076491,"db lookup returned"}], 712 | 8525}}]}] 713 | ``` 714 | 715 | ##### Delete counter (and its snapshot) 716 | 717 | ```erlang 718 | -spec counter_delete(list()) -> ok. 719 | ``` 720 | 721 | ##### Delete all counters and snapshots 722 | 723 | ```erlang 724 | -spec counter_delete_all() -> ok. 725 | ``` 726 | 727 | ## OTTER Configuration 728 | 729 | The OTTER application configuration is handled through the otter_config 730 | module (otter_config.erl). In the default implementation it uses the 731 | application environment. An example configuration can be found in the 732 | otter.app.src file. 733 | 734 | ```erlang 735 | ... 736 | {http_client, ibrowse}, %% ibrowse | httpc 737 | {zipkin_collector_uri, "http://172.17.0.2:9411/api/v1/spans"}, 738 | {zipkin_batch_interval_ms, 100}, 739 | {zipkin_tag_host_ip, {127,0,0,1}}, 740 | {zipkin_tag_host_port, 0}, 741 | {zipkin_tag_host_service, "otter_test"}, 742 | {zipkin_add_host_tag_to_span, {"lc", ""}}, 743 | {zipkin_add_default_service_to_logs, false}, 744 | {zipkin_add_default_service_to_tags, false}, 745 | {filter_rules, [ 746 | { 747 | [ 748 | {greater, otter_span_duration, 1000} 749 | ], 750 | [ 751 | {snapshot_count, [long_span], [otter_span_name]} 752 | ] 753 | }, 754 | { 755 | [], 756 | [ 757 | {snapshot_count, [span_processed], [otter_span_name]}, 758 | send_to_zipkin 759 | ] 760 | } 761 | ]}, 762 | ... 763 | ``` 764 | 765 | ## Acknowledgements 766 | 767 | The development of ''otter'' was championed by [Holger Winkelmann](https://github.com/hwinkel) of [Travelping](https://github.com/travelping). Both [Travelping](https://github.com/travelping) and [bet365](http://bet365.com) kindly provided sponsorship for the initial development. The development was primarily done by [Ferenc Holzhauser](https://github.com/fholzhauser) with design input from [Chandru Mullaparthi](https://github.com/cmullaparthi). 768 | 769 | ## License 770 | 771 | Apache 2.0 772 | -------------------------------------------------------------------------------- /doc/overview.edoc: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | @author Ferenc Holzhauser [https://github.com/fholzhauser] 3 | @author Chandru Mullaparthi [https://github.com/cmullaparthi] 4 | @author Heinz N. Gies [https://github.com/licenser] 5 | 6 | @copyright 2017, Bluehouse Technology Ltd 7 | @doc otters is an application to enable Opentracing support in Erlang. 8 | 9 | OpenTracing is an open initiative to provide a 10 | set of terms and methods to produce, collect and correlate trace 11 | information in a distributed environment across different programming 12 | languages, platforms and protocols. 13 | 14 | Otters is a fork of the 15 | otter library that focuses on community involvement, performance and code 16 | quality. 17 | 18 | @title 19 | otters - Open Tracing Toolkit for ERlang 20 | 21 | @version 22 | 0.2.0 23 | -------------------------------------------------------------------------------- /docs/images/otter_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-fifo/otters/27e063b4df776b2333a20998000b94d25ae29ccf/docs/images/otter_flow.png -------------------------------------------------------------------------------- /docs/images/otter_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-fifo/otters/27e063b4df776b2333a20998000b94d25ae29ccf/docs/images/otter_logo.png -------------------------------------------------------------------------------- /elvis.config: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | elvis, 4 | [ 5 | {config, 6 | [#{dirs => ["src", "test"], 7 | ignore => [of_parser, of_lexer], 8 | filter => "*.erl", 9 | rules => [{elvis_style, line_length, #{limit => 80, 10 | skip_comments => false}}, 11 | %% ... 12 | {elvis_style, state_record_and_type}, 13 | {elvis_style, no_spec_with_records} 14 | ] 15 | } 16 | ] 17 | } 18 | ] 19 | } 20 | ]. 21 | -------------------------------------------------------------------------------- /eqc/ol_eqc.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% Copyright (c) 2017 Heinz N. Gies 3 | %%% 4 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 5 | %%% of this software and associated documentation files (the "Software"), to 6 | %%% deal in the Software without restriction, including without limitation the 7 | %%% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 8 | %%% sell copies of the Software, and to permit persons to whom the Software is 9 | %%% furnished to do so, subject to the following conditions: 10 | %%% 11 | %%% The above copyright notice and this permission notice shall be included in 12 | %%% all copies or substantial portions of the Software.
13 | %%% 14 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING, 19 | %%% FROM OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | %%% IN THE SOFTWARE. 21 | %%% 22 | %%% @author Heinz N. Gies 23 | %%% @copyright (C) 2017, Heinz N. Gies 24 | %%%------------------------------------------------------------------- 25 | 26 | -module(ol_eqc). 27 | 28 | -compile({parse_transform,eqc_grammar}). 29 | -eqc_grammar({yecc_tokens,"../src/of_parser.yrl"}). 30 | 31 | -include_lib("eqc/include/eqc.hrl"). 32 | 33 | -compile(export_all). 34 | 35 | 36 | '('() -> 37 | {'(', nat()}. 38 | ')'() -> 39 | {')', nat()}. 40 | ','() -> 41 | {',', nat()}. 42 | '.'() -> 43 | {'.', nat()}. 44 | 45 | 46 | kw_arrow() -> 47 | {kw_arrow, nat()}. 48 | kw_cont() -> 49 | {kw_cont, nat()}. 50 | kw_count() -> 51 | {kw_count, nat()}. 52 | kw_drop() -> 53 | {kw_drop, nat()}. 54 | kw_send() -> 55 | {kw_send, nat()}. 56 | kw_skip() -> 57 | {kw_skip, nat()}. 58 | 59 | cmp() -> 60 | {cmp, nat(), oneof(['>', '>=', '<', '=<', '==', '/='])}. 61 | 62 | kw() -> 63 | {kw, nat(), kw_()}. 64 | 65 | num() -> 66 | {num, nat(), nat()}. 67 | 68 | str() -> 69 | {str, nat(), kw_()}. 70 | 71 | kw_() -> 72 | ?LET(K, ?SIZED(Size, kw_(Size)), lists:flatten([K])). 73 | 74 | kw_(0) -> 75 | [letter()]; 76 | kw_(N) -> 77 | [kw_(N - 1), oneof([letter(), choose($0, $9), $_])]. 78 | 79 | letter() -> 80 | oneof([ 81 | choose($a, $z), 82 | choose($A, $Z) 83 | ]). 84 | 85 | %% Generating: 86 | %% [{kw_arrow,0},{'(',0},{')',0},{kw_arrow,0},{kw_skip,0},{'.',0}] 87 | prop_parse() -> 88 | ?FORALL(SymbolicExpr, rule(), 89 | begin 90 | Tokens = eqc_grammar:eval(SymbolicExpr), 91 | 92 | case of_parser:parse(Tokens) of 93 | {ok, _SyntaxTree} -> 94 | true; 95 | {error, E} -> 96 | io:format("~p\n~p -> ~p~n", [SymbolicExpr, Tokens, E]), 97 | false 98 | end 99 | end). 100 | 101 | prop_compile() -> 102 | ?FORALL(SymbolicExpr, rule(), 103 | begin 104 | Tokens = eqc_grammar:eval(SymbolicExpr), 105 | {ok, Rs} = of_parser:parse(Tokens), 106 | Rs1 = ol:optimize(Rs), 107 | {ok, Cs} = ol:group_rules(Rs1), 108 | Rendered = ol:render(Cs), 109 | F = lists:flatten(Rendered), 110 | Res = try 111 | dynamic_compile:load_from_string(F) 112 | catch 113 | _:E -> 114 | E 115 | end, 116 | ?WHENFAIL(io:format(user, "~s~n=>~p~n", [F, Res]), 117 | Res =:= {module, ol_filter}) 118 | end). 119 | 120 | prop_filter() -> 121 | ?FORALL(SymbolicExpr, rule(), 122 | ?FORALL(SpanR, thrift_eqc:span(), 123 | begin 124 | Tokens = eqc_grammar:eval(SymbolicExpr), 125 | Span = eval(SpanR), 126 | Tokens = eqc_grammar:eval(SymbolicExpr), 127 | {ok, Rs} = of_parser:parse(Tokens), 128 | Rs1 = ol:optimize(Rs), 129 | {ok, Cs} = ol:group_rules(Rs1), 130 | Rendered = ol:render(Cs), 131 | F = lists:flatten(Rendered), 132 | dynamic_compile:load_from_string(F), 133 | {ok, _} = ol:run(Span), 134 | true 135 | end)). 136 | -------------------------------------------------------------------------------- /eqc/thrift_eqc.erl: -------------------------------------------------------------------------------- 1 | -module(thrift_eqc). 2 | 3 | -include_lib("eqc/include/eqc.hrl"). 4 | -include_lib("otters/include/otters.hrl"). 5 | -compile(export_all). 6 | 7 | name() -> 8 | <<"name">>. 9 | 10 | 11 | parent_id() -> 12 | oneof([nat(), undefined]). 13 | 14 | trace_id() -> 15 | int(). 16 | 17 | timestap() -> 18 | nat(). 19 | 20 | duration() -> 21 | nat(). 22 | tags() -> 23 | #{}. 24 | 25 | logs() -> 26 | []. 27 | 28 | 29 | new_span() -> 30 | #span{ 31 | id = trace_id(), 32 | timestamp = timestap(), 33 | trace_id = trace_id(), 34 | name = name(), 35 | parent_id = parent_id(), 36 | duration = duration(), 37 | tags = tags(), 38 | logs = logs() 39 | }. 40 | 41 | ip() -> 42 | {127, choose(0, 254), choose(0, 254), choose(1, 254)}. 43 | 44 | service() -> 45 | oneof([default, 46 | {binary(), ip(), nat()}, 47 | binary()]). 48 | 49 | log() -> 50 | {timestap(), binary(), service()}. 51 | 52 | tag() -> 53 | {binary(), {binary(), service()}}. 54 | 55 | info() -> 56 | oneof([binary(), 57 | ?LET(B, binary(), binary_to_list(B)), 58 | int(), 59 | real(), 60 | oneof([test, test1, hello])]). 61 | 62 | span(0) -> 63 | new_span(); 64 | 65 | 66 | span(Size) -> 67 | ?LAZY(oneof( 68 | [ 69 | {call, otters, log, [span(Size -1), info()]}, 70 | {call, otters, log, [span(Size -1), info(), service()]}, 71 | {call, otters, tag, [span(Size -1), binary(), info()]}, 72 | {call, otters, tag, [span(Size -1), binary(), info(), service()]} 73 | ])). 74 | 75 | span() -> 76 | ?SIZED(Size, span(Size)). 77 | 78 | prop_encode_decode() -> 79 | ?FORALL(Raw, list(span()), 80 | begin 81 | Spans = [eval(S) || S <- Raw], 82 | Encoded = otters_zipkin_encoder:encode(Spans), 83 | Decoded = otters_conn_zipkin:decode_spans(Encoded), 84 | Cleaned = [cleanup(S) || S <- Decoded], 85 | CleanedIn = [cleanup(S) || S <- Spans], 86 | ?WHENFAIL( 87 | io:format(user, 88 | "~p -> ~p~n", 89 | [CleanedIn, Cleaned]), 90 | CleanedIn =:= Cleaned) 91 | end). 92 | 93 | prop_encode_old() -> 94 | ?FORALL(Raw, list(span()), 95 | begin 96 | Spans = [eval(S) || S <- Raw], 97 | Encoded = otters_zipkin_encoder:encode(Spans), 98 | EncodedOld = otters_conn_zipkin:encode_spans(Spans), 99 | ?WHENFAIL( 100 | io:format(user, 101 | "~p =>\n~p =/=\n~p~n", 102 | [Spans, Encoded, EncodedOld]), 103 | Encoded =:= EncodedOld) 104 | end). 105 | 106 | 107 | prop_cmp_log() -> 108 | ?FORALL(Log, log(), 109 | begin 110 | Cfg = otters_zipkin_encoder:defaults(), 111 | Encoded = otters_zipkin_encoder:encode_log(Log, Cfg), 112 | Raw = otters_conn_zipkin:log_to_annotation(Log), 113 | EncodedOld = otters_conn_zipkin:encode({struct, Raw}), 114 | ?WHENFAIL( 115 | io:format(user, 116 | "~p =>\n~p =/=\n~p~n", 117 | [Log, Encoded, EncodedOld]), 118 | Encoded =:= EncodedOld) 119 | end). 120 | 121 | prop_cmp_tag() -> 122 | ?FORALL(Tag, tag(), 123 | begin 124 | Cfg = otters_zipkin_encoder:defaults(), 125 | Encoded = otters_zipkin_encoder:encode_tag(Tag, Cfg), 126 | Raw = otters_conn_zipkin:tag_to_binary_annotation(Tag), 127 | EncodedOld = otters_conn_zipkin:encode({struct, Raw}), 128 | ?WHENFAIL( 129 | io:format(user, 130 | "~p =>\n~p =/=\n~p~n", 131 | [Tag, Encoded, EncodedOld]), 132 | Encoded =:= EncodedOld) 133 | end). 134 | 135 | cleanup(S = #span{ 136 | tags = Tags, 137 | logs = Logs 138 | }) -> 139 | S#span{ 140 | tags = clean_tags(Tags), 141 | logs = clean_logs(Logs) 142 | }. 143 | 144 | clean_tags(Tags) -> 145 | maps:map(fun (_, {V, {<<"otters">>, {127,0,0,1}, 0}}) -> 146 | {otters_lib:to_bin(V), default}; 147 | (_, {V, {S, {127,0,0,1}, 0}}) -> 148 | {otters_lib:to_bin(V), S}; 149 | (_, {V, T}) -> 150 | {otters_lib:to_bin(V), T}; 151 | (_, V) -> 152 | otters_lib:to_bin(V) 153 | end, maps:remove(<<"lc">>, Tags)). 154 | 155 | clean_logs(Logs) -> 156 | [clean_log(L) || L <- Logs]. 157 | 158 | 159 | clean_log({T, V, {<<"otters">>, {127,0,0,1}, 0}}) -> 160 | {T, otters_lib:to_bin(V), default}; 161 | clean_log({T, V, {S, {127,0,0,1}, 0}}) -> 162 | {T, otters_lib:to_bin(V), S}; 163 | clean_log({T, V}) -> 164 | {T, otters_lib:to_bin(V), undefined}; 165 | clean_log(O) -> 166 | O. 167 | -------------------------------------------------------------------------------- /include/otters.hrl: -------------------------------------------------------------------------------- 1 | -record(span, { 2 | %% strt timestamp 3 | timestamp :: otters:time_us(), 4 | %% 64 bit int trace id 5 | trace_id :: otters:trace_id() | undefined, 6 | %% name of the span 7 | name :: otters:info(), 8 | %% 64 bit int span id 9 | id :: otters:span_id() | undefined, 10 | %% 64 bit int parent span 11 | parent_id :: otters:span_id() | undefined, 12 | %% Tags 13 | tags = #{} :: otter:tags(), 14 | %% logs 15 | logs = [] :: [{otters:info(), otters:info(), 16 | otters:service()}], 17 | %% microseconds between span start/end 18 | duration :: otters:time_us() 19 | }). 20 | -------------------------------------------------------------------------------- /priv/otters.schema: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | 3 | %% @doc How often otters will scan the cache and send batch data 4 | %% to zapkin. 5 | {mapping, "otters.batch_interval", "otters.zipkin_batch_interval_ms", 6 | [{default, "100ms"}, 7 | {datatype, {duration, ms}}]}. 8 | 9 | %% @doc the Zapkin URL. 10 | {mapping, "otters.zapkin_url", "otters.zipkin_collector_uri", 11 | [{default, "http://127.0.0.1:9411/api/v1/spans"}, 12 | {datatype, string}]}. 13 | 14 | %% @doc The host otters will declare to send datas from. 15 | {mapping, "otters.host", "otters.zipkin_tag_host_ip", 16 | [{default, "127.0.0.1:0"}, 17 | {datatype, ip}]}. 18 | 19 | {translation, 20 | "otters.zipkin_tag_host_ip", 21 | fun(Conf) -> 22 | {IPs, _Port} = cuttlefish:conf_get("otters.host", Conf), 23 | {ok, IP} = inet:parse_ipv4_address(IPs), 24 | IP 25 | end 26 | }. 27 | 28 | {translation, 29 | "otters.zipkin_tag_host_port", 30 | fun(Conf) -> 31 | {_Host, Port} = cuttlefish:conf_get("otters.host", Conf), 32 | Port 33 | end 34 | }. 35 | 36 | %% @doc Default service name otter reports 37 | {mapping, "otters.service", "otters.zipkin_tag_host_service", 38 | [{default, "{{service}}"}, 39 | {datatype, string}]}. 40 | 41 | %% @doc Weather or not to add the default host to logs or not 42 | {mapping, "otters.add_service_to_log", 43 | "otters.zipkin_add_default_service_to_logs", 44 | [{default, "off"}, 45 | {datatype, flag}]}. 46 | 47 | %% @doc Weather or not to add the default host to tags or not 48 | {mapping, "otters.add_service_to_tags", 49 | "otters.zipkin_add_default_service_to_tags", 50 | [{default, "off"}, 51 | {datatype, flag}]}. 52 | 53 | %% @doc Default service logs or tags are tagged with. 54 | {mapping, "otters.default_key", "otters.zipkin_add_host_tag_to_span", 55 | [{default, "lc"}, 56 | {datatype, string}]}. 57 | 58 | %% @doc Default service logs or tags are tagged with. 59 | {mapping, "otters.default_value", "otters.zipkin_add_host_tag_to_span", 60 | [{default, "v"}, 61 | {datatype, string}]}. 62 | 63 | {translation, 64 | "otters.zipkin_add_host_tag_to_span", 65 | fun(Conf) -> 66 | K = cuttlefish:conf_get("otters.default_key", Conf), 67 | V = cuttlefish:conf_get("otters.default_value", Conf), 68 | {K, V} 69 | end 70 | }. 71 | 72 | %% @doc File to read filter rules from 73 | {mapping, "otters.filter", "otters.filter_file", 74 | [{commented, "rules.ot"}, 75 | {datatype, file}]}. 76 | -------------------------------------------------------------------------------- /priv/zipkinCore.thrift: -------------------------------------------------------------------------------- 1 | # Copyright 2012 Twitter Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | namespace java com.twitter.zipkin.thriftjava 15 | #@namespace scala com.twitter.zipkin.thriftscala 16 | namespace rb Zipkin 17 | 18 | #************** Annotation.value ************** 19 | /** 20 | * The client sent ("cs") a request to a server. There is only one send per 21 | * span. For example, if there's a transport error, each attempt can be logged 22 | * as a WIRE_SEND annotation. 23 | * 24 | * If chunking is involved, each chunk could be logged as a separate 25 | * CLIENT_SEND_FRAGMENT in the same span. 26 | * 27 | * Annotation.host is not the server. It is the host which logged the send 28 | * event, almost always the client. When logging CLIENT_SEND, instrumentation 29 | * should also log the SERVER_ADDR. 30 | */ 31 | const string CLIENT_SEND = "cs" 32 | /** 33 | * The client received ("cr") a response from a server. There is only one 34 | * receive per span. For example, if duplicate responses were received, each 35 | * can be logged as a WIRE_RECV annotation. 36 | * 37 | * If chunking is involved, each chunk could be logged as a separate 38 | * CLIENT_RECV_FRAGMENT in the same span. 39 | * 40 | * Annotation.host is not the server. It is the host which logged the receive 41 | * event, almost always the client. The actual endpoint of the server is 42 | * recorded separately as SERVER_ADDR when CLIENT_SEND is logged. 43 | */ 44 | const string CLIENT_RECV = "cr" 45 | /** 46 | * The server sent ("ss") a response to a client. There is only one response 47 | * per span. If there's a transport error, each attempt can be logged as a 48 | * WIRE_SEND annotation. 49 | * 50 | * Typically, a trace ends with a server send, so the last timestamp of a trace 51 | * is often the timestamp of the root span's server send. 52 | * 53 | * If chunking is involved, each chunk could be logged as a separate 54 | * SERVER_SEND_FRAGMENT in the same span. 55 | * 56 | * Annotation.host is not the client. It is the host which logged the send 57 | * event, almost always the server. The actual endpoint of the client is 58 | * recorded separately as CLIENT_ADDR when SERVER_RECV is logged. 59 | */ 60 | const string SERVER_SEND = "ss" 61 | /** 62 | * The server received ("sr") a request from a client. There is only one 63 | * request per span. For example, if duplicate responses were received, each 64 | * can be logged as a WIRE_RECV annotation. 65 | * 66 | * Typically, a trace starts with a server receive, so the first timestamp of a 67 | * trace is often the timestamp of the root span's server receive. 68 | * 69 | * If chunking is involved, each chunk could be logged as a separate 70 | * SERVER_RECV_FRAGMENT in the same span. 71 | * 72 | * Annotation.host is not the client. It is the host which logged the receive 73 | * event, almost always the server. When logging SERVER_RECV, instrumentation 74 | * should also log the CLIENT_ADDR. 75 | */ 76 | const string SERVER_RECV = "sr" 77 | /** 78 | * Optionally logs an attempt to send a message on the wire. Multiple wire send 79 | * events could indicate network retries. A lag between client or server send 80 | * and wire send might indicate queuing or processing delay. 81 | */ 82 | const string WIRE_SEND = "ws" 83 | /** 84 | * Optionally logs an attempt to receive a message from the wire. Multiple wire 85 | * receive events could indicate network retries. A lag between wire receive 86 | * and client or server receive might indicate queuing or processing delay. 87 | */ 88 | const string WIRE_RECV = "wr" 89 | /** 90 | * Optionally logs progress of a (CLIENT_SEND, WIRE_SEND). For example, this 91 | * could be one chunk in a chunked request. 92 | */ 93 | const string CLIENT_SEND_FRAGMENT = "csf" 94 | /** 95 | * Optionally logs progress of a (CLIENT_RECV, WIRE_RECV). For example, this 96 | * could be one chunk in a chunked response. 97 | */ 98 | const string CLIENT_RECV_FRAGMENT = "crf" 99 | /** 100 | * Optionally logs progress of a (SERVER_SEND, WIRE_SEND). For example, this 101 | * could be one chunk in a chunked response. 102 | */ 103 | const string SERVER_SEND_FRAGMENT = "ssf" 104 | /** 105 | * Optionally logs progress of a (SERVER_RECV, WIRE_RECV). For example, this 106 | * could be one chunk in a chunked request. 107 | */ 108 | const string SERVER_RECV_FRAGMENT = "srf" 109 | 110 | #***** BinaryAnnotation.key ****** 111 | /** 112 | * The domain portion of the URL or host header. Ex. "mybucket.s3.amazonaws.com" 113 | * 114 | * Used to filter by host as opposed to ip address. 115 | */ 116 | const string HTTP_HOST = "http.host" 117 | 118 | /** 119 | * The HTTP method, or verb, such as "GET" or "POST". 120 | * 121 | * Used to filter against an http route. 122 | */ 123 | const string HTTP_METHOD = "http.method" 124 | 125 | /** 126 | * The absolute http path, without any query parameters. Ex. "/objects/abcd-ff" 127 | * 128 | * Used to filter against an http route, portably with zipkin v1. 129 | * 130 | * In zipkin v1, only equals filters are supported. Dropping query parameters makes the number 131 | * of distinct URIs less. For example, one can query for the same resource, regardless of signing 132 | * parameters encoded in the query line. This does not reduce cardinality to a HTTP single route. 133 | * For example, it is common to express a route as an http URI template like 134 | * "/resource/{resource_id}". In systems where only equals queries are available, searching for 135 | * http/path=/resource won't match if the actual request was /resource/abcd-ff. 136 | * 137 | * Historical note: This was commonly expressed as "http.uri" in zipkin, eventhough it was most 138 | * often just a path. 139 | */ 140 | const string HTTP_PATH = "http.path" 141 | 142 | /** 143 | * The entire URL, including the scheme, host and query parameters if available. Ex. 144 | * "https://mybucket.s3.amazonaws.com/objects/abcd-ff?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Algorithm=AWS4-HMAC-SHA256..." 145 | * 146 | * Combined with HTTP_METHOD, you can understand the fully-qualified request line. 147 | * 148 | * This is optional as it may include private data or be of considerable length. 149 | */ 150 | const string HTTP_URL = "http.url" 151 | 152 | /** 153 | * The HTTP status code, when not in 2xx range. Ex. "503" 154 | * 155 | * Used to filter for error status. 156 | */ 157 | const string HTTP_STATUS_CODE = "http.status_code" 158 | 159 | /** 160 | * The size of the non-empty HTTP request body, in bytes. Ex. "16384" 161 | * 162 | * Large uploads can exceed limits or contribute directly to latency. 163 | */ 164 | const string HTTP_REQUEST_SIZE = "http.request.size" 165 | 166 | /** 167 | * The size of the non-empty HTTP response body, in bytes. Ex. "16384" 168 | * 169 | * Large downloads can exceed limits or contribute directly to latency. 170 | */ 171 | const string HTTP_RESPONSE_SIZE = "http.response.size" 172 | 173 | /** 174 | * The value of "lc" is the component or namespace of a local span. 175 | * 176 | * BinaryAnnotation.host adds service context needed to support queries. 177 | * 178 | * Local Component("lc") supports three key features: flagging, query by 179 | * service and filtering Span.name by namespace. 180 | * 181 | * While structurally the same, local spans are fundamentally different than 182 | * RPC spans in how they should be interpreted. For example, zipkin v1 tools 183 | * center on RPC latency and service graphs. Root local-spans are neither 184 | * indicative of critical path RPC latency, nor have impact on the shape of a 185 | * service graph. By flagging with "lc", tools can special-case local spans. 186 | * 187 | * Zipkin v1 Spans are unqueryable unless they can be indexed by service name. 188 | * The only path to a service name is by (Binary)?Annotation.host.serviceName. 189 | * By logging "lc", a local span can be queried even if no other annotations 190 | * are logged. 191 | * 192 | * The value of "lc" is the namespace of Span.name. For example, it might be 193 | * "finatra2", for a span named "bootstrap". "lc" allows you to resolves 194 | * conflicts for the same Span.name, for example "finatra/bootstrap" vs 195 | * "finch/bootstrap". Using local component, you'd search for spans named 196 | * "bootstrap" where "lc=finch" 197 | */ 198 | const string LOCAL_COMPONENT = "lc" 199 | 200 | #***** Annotation.value or BinaryAnnotation.key ****** 201 | /** 202 | * When an annotation value, this indicates when an error occurred. When a 203 | * binary annotation key, the value is a human readable message associated 204 | * with an error. 205 | * 206 | * Due to transient errors, an ERROR annotation should not be interpreted 207 | * as a span failure, even the annotation might explain additional latency. 208 | * Instrumentation should add the ERROR binary annotation when the operation 209 | * failed and couldn't be recovered. 210 | * 211 | * Here's an example: A span has an ERROR annotation, added when a WIRE_SEND 212 | * failed. Another WIRE_SEND succeeded, so there's no ERROR binary annotation 213 | * on the span because the overall operation succeeded. 214 | * 215 | * Note that RPC spans often include both client and server hosts: It is 216 | * possible that only one side perceived the error. 217 | */ 218 | const string ERROR = "error" 219 | 220 | #***** BinaryAnnotation.key where value = [1] and annotation_type = BOOL ****** 221 | /** 222 | * Indicates a client address ("ca") in a span. Most likely, there's only one. 223 | * Multiple addresses are possible when a client changes its ip or port within 224 | * a span. 225 | */ 226 | const string CLIENT_ADDR = "ca" 227 | /** 228 | * Indicates a server address ("sa") in a span. Most likely, there's only one. 229 | * Multiple addresses are possible when a client is redirected, or fails to a 230 | * different server ip or port. 231 | */ 232 | const string SERVER_ADDR = "sa" 233 | 234 | /** 235 | * Indicates the network context of a service recording an annotation with two 236 | * exceptions. 237 | * 238 | * When a BinaryAnnotation, and key is CLIENT_ADDR or SERVER_ADDR, 239 | * the endpoint indicates the source or destination of an RPC. This exception 240 | * allows zipkin to display network context of uninstrumented services, or 241 | * clients such as web browsers. 242 | */ 243 | struct Endpoint { 244 | /** 245 | * IPv4 host address packed into 4 bytes. 246 | * 247 | * Ex for the ip 1.2.3.4, it would be (1 << 24) | (2 << 16) | (3 << 8) | 4 248 | */ 249 | 1: i32 ipv4 250 | /** 251 | * IPv4 port or 0, if unknown. 252 | * 253 | * Note: this is to be treated as an unsigned integer, so watch for negatives. 254 | */ 255 | 2: i16 port 256 | /** 257 | * Classifier of a source or destination in lowercase, such as "zipkin-web". 258 | * 259 | * This is the primary parameter for trace lookup, so should be intuitive as 260 | * possible, for example, matching names in service discovery. 261 | * 262 | * Conventionally, when the service name isn't known, service_name = "unknown". 263 | * However, it is also permissible to set service_name = "" (empty string). 264 | * The difference in the latter usage is that the span will not be queryable 265 | * by service name unless more information is added to the span with non-empty 266 | * service name, e.g. an additional annotation from the server. 267 | * 268 | * Particularly clients may not have a reliable service name at ingest. One 269 | * approach is to set service_name to "" at ingest, and later assign a 270 | * better label based on binary annotations, such as user agent. 271 | */ 272 | 3: string service_name 273 | /** 274 | * IPv6 host address packed into 16 bytes. Ex Inet6Address.getBytes() 275 | */ 276 | 4: optional binary ipv6 277 | } 278 | 279 | /** 280 | * Associates an event that explains latency with a timestamp. 281 | * 282 | * Unlike log statements, annotations are often codes: for example "sr". 283 | */ 284 | struct Annotation { 285 | /** 286 | * Microseconds from epoch. 287 | * 288 | * This value should use the most precise value possible. For example, 289 | * gettimeofday or multiplying currentTimeMillis by 1000. 290 | */ 291 | 1: i64 timestamp 292 | /** 293 | * Usually a short tag indicating an event, like "sr" or "finagle.retry". 294 | */ 295 | 2: string value 296 | /** 297 | * The host that recorded the value, primarily for query by service name. 298 | */ 299 | 3: optional Endpoint host 300 | // don't reuse 4: optional i32 OBSOLETE_duration // how long did the operation take? microseconds 301 | } 302 | 303 | /** 304 | * A subset of thrift base types, except BYTES. 305 | */ 306 | enum AnnotationType { 307 | /** 308 | * Set to 0x01 when key is CLIENT_ADDR or SERVER_ADDR 309 | */ 310 | BOOL, 311 | /** 312 | * No encoding, or type is unknown. 313 | */ 314 | BYTES, 315 | I16, 316 | I32, 317 | I64, 318 | DOUBLE, 319 | /** 320 | * the only type zipkin v1 supports search against. 321 | */ 322 | STRING 323 | } 324 | 325 | /** 326 | * Binary annotations are tags applied to a Span to give it context. For 327 | * example, a binary annotation of HTTP_PATH ("http.path") could the path 328 | * to a resource in a RPC call. 329 | * 330 | * Binary annotations of type STRING are always queryable, though more a 331 | * historical implementation detail than a structural concern. 332 | * 333 | * Binary annotations can repeat, and vary on the host. Similar to Annotation, 334 | * the host indicates who logged the event. This allows you to tell the 335 | * difference between the client and server side of the same key. For example, 336 | * the key "http.path" might be different on the client and server side due to 337 | * rewriting, like "/api/v1/myresource" vs "/myresource. Via the host field, 338 | * you can see the different points of view, which often help in debugging. 339 | */ 340 | struct BinaryAnnotation { 341 | /** 342 | * Name used to lookup spans, such as "http.path" or "finagle.version". 343 | */ 344 | 1: string key, 345 | /** 346 | * Serialized thrift bytes, in TBinaryProtocol format. 347 | * 348 | * For legacy reasons, byte order is big-endian. See THRIFT-3217. 349 | */ 350 | 2: binary value, 351 | /** 352 | * The thrift type of value, most often STRING. 353 | * 354 | * annotation_type shouldn't vary for the same key. 355 | */ 356 | 3: AnnotationType annotation_type, 357 | /** 358 | * The host that recorded value, allowing query by service name or address. 359 | * 360 | * There are two exceptions: when key is "ca" or "sa", this is the source or 361 | * destination of an RPC. This exception allows zipkin to display network 362 | * context of uninstrumented services, such as browsers or databases. 363 | */ 364 | 4: optional Endpoint host 365 | } 366 | 367 | /** 368 | * A trace is a series of spans (often RPC calls) which form a latency tree. 369 | * 370 | * Spans are usually created by instrumentation in RPC clients or servers, but 371 | * can also represent in-process activity. Annotations in spans are similar to 372 | * log statements, and are sometimes created directly by application developers 373 | * to indicate events of interest, such as a cache miss. 374 | * 375 | * The root span is where parent_id = Nil; it usually has the longest duration 376 | * in the trace. 377 | * 378 | * Span identifiers are packed into i64s, but should be treated opaquely. 379 | * String encoding is fixed-width lower-hex, to avoid signed interpretation. 380 | */ 381 | struct Span { 382 | /** 383 | * Unique 8-byte identifier for a trace, set on all spans within it. 384 | */ 385 | 1: i64 trace_id 386 | /** 387 | * Span name in lowercase, rpc method for example. Conventionally, when the 388 | * span name isn't known, name = "unknown". 389 | */ 390 | 3: string name, 391 | /** 392 | * Unique 8-byte identifier of this span within a trace. A span is uniquely 393 | * identified in storage by (trace_id, id). 394 | */ 395 | 4: i64 id, 396 | /** 397 | * The parent's Span.id; absent if this the root span in a trace. 398 | */ 399 | 5: optional i64 parent_id, 400 | /** 401 | * Associates events that explain latency with a timestamp. Unlike log 402 | * statements, annotations are often codes: for example SERVER_RECV("sr"). 403 | * Annotations are sorted ascending by timestamp. 404 | */ 405 | 6: list annotations, 406 | /** 407 | * Tags a span with context, usually to support query or aggregation. For 408 | * example, a binary annotation key could be "http.path". 409 | */ 410 | 8: list binary_annotations 411 | /** 412 | * True is a request to store this span even if it overrides sampling policy. 413 | */ 414 | 9: optional bool debug = 0 415 | /** 416 | * Epoch microseconds of the start of this span, absent if this an incomplete 417 | * span. 418 | * 419 | * This value should be set directly by instrumentation, using the most 420 | * precise value possible. For example, gettimeofday or syncing nanoTime 421 | * against a tick of currentTimeMillis. 422 | * 423 | * For compatibilty with instrumentation that precede this field, collectors 424 | * or span stores can derive this via Annotation.timestamp. 425 | * For example, SERVER_RECV.timestamp or CLIENT_SEND.timestamp. 426 | * 427 | * Timestamp is nullable for input only. Spans without a timestamp cannot be 428 | * presented in a timeline: Span stores should not output spans missing a 429 | * timestamp. 430 | * 431 | * There are two known edge-cases where this could be absent: both cases 432 | * exist when a collector receives a span in parts and a binary annotation 433 | * precedes a timestamp. This is possible when.. 434 | * - The span is in-flight (ex not yet received a timestamp) 435 | * - The span's start event was lost 436 | */ 437 | 10: optional i64 timestamp, 438 | /** 439 | * Measurement in microseconds of the critical path, if known. Durations of 440 | * less than one microsecond must be rounded up to 1 microsecond. 441 | * 442 | * This value should be set directly, as opposed to implicitly via annotation 443 | * timestamps. Doing so encourages precision decoupled from problems of 444 | * clocks, such as skew or NTP updates causing time to move backwards. 445 | * 446 | * For compatibility with instrumentation that precede this field, collectors 447 | * or span stores can derive this by subtracting Annotation.timestamp. 448 | * For example, SERVER_SEND.timestamp - SERVER_RECV.timestamp. 449 | * 450 | * If this field is persisted as unset, zipkin will continue to work, except 451 | * duration query support will be implementation-specific. Similarly, setting 452 | * this field non-atomically is implementation-specific. 453 | * 454 | * This field is i64 vs i32 to support spans longer than 35 minutes. 455 | */ 456 | 11: optional i64 duration 457 | /** 458 | * Optional unique 8-byte additional identifier for a trace. If non zero, this 459 | * means the trace uses 128 bit traceIds instead of 64 bit. 460 | */ 461 | 12: optional i64 trace_id_high 462 | } 463 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info]}. 2 | {deps, [{ibrowse, "~>4.4.0"}, {dynamic_compile, "~>1.0.0"}]}. 3 | 4 | {xref_checks,[undefined_function_calls,undefined_functions,locals_not_used, 5 | deprecated_function_calls, deprecated_functions]}. 6 | 7 | 8 | {profiles, 9 | [{test, [{deps, [meck]}]}, 10 | {eqc, [{erl_opts, [{d, 'TEST'}]}, {plugins, [rebar_eqc]}]}, 11 | {shell, [{deps, [sync]}]}, 12 | {lint, 13 | [{plugins, 14 | [rebar3_lint]}]}]}. 15 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.1.0", 2 | [{<<"dynamic_compile">>,{pkg,<<"dynamic_compile">>,<<"1.0.0">>},0}, 3 | {<<"ibrowse">>,{pkg,<<"ibrowse">>,<<"4.4.0">>},0}]}. 4 | [ 5 | {pkg_hash,[ 6 | {<<"dynamic_compile">>, <<"8171B2CB4953EA3ED2EF63F5B26ABF677ACD0CA32210C2A08A7A8406A743F76B">>}, 7 | {<<"ibrowse">>, <<"2D923325EFE0D2CB09B9C6A047B2835A5EDA69D8A47ED6FF8BC03628B764E991">>}]} 8 | ]. 9 | -------------------------------------------------------------------------------- /rebar3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-fifo/otters/27e063b4df776b2333a20998000b94d25ae29ccf/rebar3 -------------------------------------------------------------------------------- /src/of_lexer.xrl: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | %%%------------------------------------------------------------------- 3 | %%% Copyright (c) 2017 Heinz N. Gies 4 | %%% 5 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 6 | %%% of this software and associated documentation files (the "Software"), to 7 | %%% deal in the Software without restriction, including without limitation the 8 | %%% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | %%% sell copies of the Software, and to permit persons to whom the Software is 10 | %%% furnished to do so, subject to the following conditions: 11 | %%% 12 | %%% The above copyright notice and this permission notice shall be included in 13 | %%% all copies or substantial portions of the Software.
14 | %%% 15 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING, 20 | %%% FROM OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | %%% IN THE SOFTWARE. 22 | %%% 23 | %%% @author Heinz N. Gies 24 | %%% @copyright (C) 2017, Heinz N. Gies 25 | %%%------------------------------------------------------------------- 26 | 27 | Definitions. 28 | NUM = [0-9]+ 29 | WS = ([\000-\s]|%.*) 30 | Str1 = '([^']|\.)+' 31 | %'% damn you syntax highlighter 32 | Str2 = "([^"]|\.)+" 33 | %"% damn you syntax highlighter 34 | Instance = instance 35 | KW = [A-Za-z][A-Za-z0-9_]* 36 | Comp = (>|>=|<|=<|==|/=) 37 | Arrow = [-][>] 38 | Skip = skip 39 | Drop = drop 40 | Send = send 41 | Cont = continue 42 | Count = count 43 | At = [@] 44 | 45 | 46 | Rules. 47 | %%{ERL} : {token, {erlang, TokenLine, unerl(TokenChars, TokenLine)}}. 48 | {Comment} : {token, {comment, TokenLine}}. 49 | {Arrow} : {token, {kw_arrow, TokenLine}}. 50 | {Send} : {token, {kw_send, TokenLine}}. 51 | {Drop} : {token, {kw_drop, TokenLine}}. 52 | {Skip} : {token, {kw_skip, TokenLine}}. 53 | {Cont} : {token, {kw_cont, TokenLine}}. 54 | {Count} : {token, {kw_count, TokenLine}}. 55 | {KW} : {token, {kw, TokenLine, TokenChars}}. 56 | {Comp} : {token, {cmp, TokenLine, a(TokenChars)}}. 57 | {NUM} : {token, {num, TokenLine, i(TokenChars)}}. 58 | {Str1} : S = strip(TokenChars, TokenLen), 59 | {token, {str, TokenLine, S}}. 60 | {Str2} : S = strip(TokenChars, TokenLen), 61 | {token, {str, TokenLine, S}}. 62 | 63 | [(),.] : {token, {a(TokenChars), TokenLine}}. 64 | {WS}+ : skip_token. 65 | 66 | 67 | Erlang code. 68 | 69 | -ignore_xref([format_error/1, string/2, token/2, token/3, tokens/2, tokens/3]). 70 | 71 | strip(TokenChars, TokenLen) -> lists:sublist(TokenChars, 2, TokenLen - 2). 72 | %%unerl(TokenChars, TokenLen) -> lists:sublist(TokenChars, 6, TokenLen). 73 | 74 | a(L) -> list_to_atom(L). 75 | i(L) -> list_to_integer(L). 76 | 77 | -dialyzer({nowarn_function, yyrev/2}). 78 | -------------------------------------------------------------------------------- /src/of_parser.yrl: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | %%%------------------------------------------------------------------- 3 | %%% Copyright (c) 2017 Heinz N. Gies 4 | %%% 5 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 6 | %%% of this software and associated documentation files (the "Software"), to 7 | %%% deal in the Software without restriction, including without limitation the 8 | %%% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | %%% sell copies of the Software, and to permit persons to whom the Software is 10 | %%% furnished to do so, subject to the following conditions: 11 | %%% 12 | %%% The above copyright notice and this permission notice shall be included in 13 | %%% all copies or substantial portions of the Software.
14 | %%% 15 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING, 20 | %%% FROM OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | %%% IN THE SOFTWARE. 22 | %%% 23 | %%% @author Heinz N. Gies 24 | %%% @copyright (C) 2017, Heinz N. Gies 25 | %%%------------------------------------------------------------------- 26 | 27 | Nonterminals 28 | rule rules condition action keyword name count_path count_element. 29 | 30 | Terminals 31 | '(' ')' ',' '.' str kw cmp num kw_skip kw_arrow kw_count kw_send 32 | kw_drop kw_cont. 33 | 34 | %%%=================================================================== 35 | %%% Root statement 36 | %%%=================================================================== 37 | Rootsymbol rules. 38 | 39 | rules -> rule : ['$1']. 40 | rules -> rule rules : ['$1'] ++ '$2'. 41 | 42 | name -> keyword : '$1'. 43 | name -> kw_drop : "drop". 44 | name -> kw_send : "send". 45 | name -> kw_cont : "continue". 46 | name -> kw_count : "count". 47 | name -> kw_skip : "skip". 48 | 49 | keyword -> kw : unwrap('$1'). 50 | 51 | rule -> name '(' condition ')' kw_arrow action '.' : {'$1', '$3', '$6'}. 52 | rule -> name '(' ')' kw_arrow action '.' : {'$1', undefined, '$5'}. 53 | 54 | 55 | condition -> keyword : {exists, '$1'}. 56 | condition -> keyword cmp num : {unwrap('$2'), '$1', unwrap('$3')}. 57 | condition -> keyword cmp str : {unwrap('$2'), '$1', unwrap('$3')}. 58 | 59 | 60 | action -> kw_skip : skip. 61 | action -> kw_drop : drop. 62 | action -> kw_send : send. 63 | action -> kw_cont : continue. 64 | action -> kw_count '(' count_path ')' : {count, '$3'}. 65 | 66 | count_element -> keyword : {get, '$1'}. 67 | count_element -> str : unwrap('$1'). 68 | 69 | count_path -> count_element : ['$1']. 70 | count_path -> count_element ',' count_path : ['$1'] ++ '$3'. 71 | 72 | 73 | %%%=================================================================== 74 | %%% Erlang code. 75 | %%%=================================================================== 76 | 77 | Erlang code. 78 | -ignore_xref([format_error/1, parse_and_scan/1, return_error/2]). 79 | 80 | unwrap({_,_,V}) -> V; 81 | unwrap({_, V}) -> V. 82 | -------------------------------------------------------------------------------- /src/ol.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% Copyright (c) 2017 Heinz N. Gies 3 | %%% 4 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 5 | %%% of this software and associated documentation files (the "Software"), to 6 | %%% deal in the Software without restriction, including without limitation the 7 | %%% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 8 | %%% sell copies of the Software, and to permit persons to whom the Software is 9 | %%% furnished to do so, subject to the following conditions: 10 | %%% 11 | %%% The above copyright notice and this permission notice shall be included in 12 | %%% all copies or substantial portions of the Software.
13 | %%% 14 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING, 19 | %%% FROM OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | %%% IN THE SOFTWARE. 21 | %%% 22 | %%% @author Heinz N. Gies 23 | %%% @copyright (C) 2017, Heinz N. Gies 24 | %%% @doc The ol module is the interface to the otters filter language 25 | %%% it deals with compiling and running compiled filters. 26 | %%% 27 | %%% The otters filter language is a high peroformance language build to 28 | %%% filter open traccing spans. It achives this by compiling directoy to 29 | %%% erlang code that can be run ontop of the erlang virutal machine instead 30 | %%% taking advantage of pattern matching, guards and other features that 31 | %%% are already implemented within the erlang vm. 32 | %%% 33 | %%% The language itself is loosely baed on erlang syntax, and works somewaht 34 | %%% like firewall rules. It comes with two main abstractions, the rule 35 | %%% and the condition, while rules are sets of one or more conditions. 36 | %%% During rule execution all tags can be accessed and compared against, as well 37 | %%% as the two special values otters_span_duration and 38 | %%% otters_span_name reflecting respectively the duration and the 39 | %%% name of the span. 40 | %%% 41 | %%% Rules are executed in order they are written, within the rules conditions 42 | %%% are also excuted within the order they're provided. 43 | %%% 44 | %%% Each rule can have a condition attached, which compares a tag with a value 45 | %%% values can be either strings or numbers. If the condition just is a field 46 | %%% the existence will be tested, if the condition is empty the condition is 47 | %%% always executed once it is reached. 48 | %%% 49 | %%% Each condition can have an action attached. Each action is only excuted 50 | %%% once, so if two rules end with a send action or a count 51 | %%% action for the same counter only one of them is executed. However 52 | %%% count for different counters are all excutred. Possible actions are: 53 | %%%
54 | %%%
drop
55 | %%%
Drops this spawn, does not excute futher conditions in this rule 56 | %%% neither does it excute furhter rules.
57 | %%%
skip
58 | %%%
Ships the rest of the rule but continues with the next rule.
59 | %%%
continue
60 | %%%
If the condition matches it continues the current rule, otherwise 61 | %%% skips therest of the rule.
62 | %%%
send
63 | %%%
Marks the span to be send.
64 | %%%
count(string | tag ...)
65 | %%%
Coun the current span with a given counter name, elements can either 66 | %%% be a literal string, or a name of a field that will be looked up.
67 | %%%
68 | %%% 69 | %%% An example ruleset would be: 70 | %%% 71 | %%%
 72 | %%% %% If our span takes less then 5s skip the rest of the rules
 73 | %%% slow_spans(otters_span_duration > 5000000) ->
 74 | %%%   continue.
 75 | %%% %% Skip requests that are not radius requests
 76 | %%% slow_spans(otters_span_name == 'radius request') ->
 77 | %%%   continue.
 78 | %%% %% Count
 79 | %%% slow_spans() ->
 80 | %%%   count('long_radius_request').
 81 | %%% %% Send
 82 | %%% slow_spans() ->
 83 | %%%   send.
 84 | %%% %% Count them all
 85 | %%% count() ->
 86 | %%%   count('request', otters_span_name, final_result).
 87 | %%% 
88 | %%% @end 89 | %%% Created : 14 Apr 2017 by Heinz N. Gies 90 | %%%------------------------------------------------------------------- 91 | -module(ol). 92 | -include_lib("otters/include/otters.hrl"). 93 | -export([span/1, load/1, compile/1, clear/0]). 94 | 95 | -define(DURATION, "otters_span_duration"). 96 | -define(NAME, "otters_span_name"). 97 | 98 | -ifdef(TEST). 99 | -compile(export_all). 100 | -endif. 101 | 102 | %%-------------------------------------------------------------------- 103 | %% @doc 104 | %% Loads a filter rule file and compiles it 105 | %% @end 106 | %%-------------------------------------------------------------------- 107 | load(F) -> 108 | {ok, B} = file:read_file(F), 109 | S = binary_to_list(B), 110 | compile(S). 111 | 112 | %%-------------------------------------------------------------------- 113 | %% @doc 114 | %% Compiles a filter script and generates the related module. 115 | %% @end 116 | %%-------------------------------------------------------------------- 117 | compile(S) -> 118 | {ok, T, _} = of_lexer:string(S), 119 | {ok, Rs} = of_parser:parse(T), 120 | Rs1 = optimize(Rs), 121 | {ok, Cs} = group_rules(Rs1), 122 | Rendered = render(Cs), 123 | %%io:format("~s~n", [Rendered]), 124 | application:set_env(otters, filter_string, S), 125 | case dynamic_compile:load_from_string(lists:flatten(Rendered)) of 126 | {module, ol_filter} -> 127 | ok; 128 | E -> 129 | E 130 | end. 131 | 132 | %%-------------------------------------------------------------------- 133 | %% @doc 134 | %% Removes the filter script and module. 135 | %% @end 136 | %%-------------------------------------------------------------------- 137 | clear() -> 138 | compile("drop() -> drop."). 139 | 140 | %%-------------------------------------------------------------------- 141 | %% @doc 142 | %% Tests a span and performs the requested actions on it. 143 | %% @end 144 | %%-------------------------------------------------------------------- 145 | span(Span) -> 146 | {ok, Actions} = run(Span), 147 | perform(lists:usort(Actions), Span). 148 | 149 | %%%=================================================================== 150 | %%% Internal functions 151 | %%%=================================================================== 152 | 153 | optimize([{_,undefined,continue} | R]) -> 154 | optimize(R); 155 | optimize([]) -> 156 | []; 157 | optimize([Rule | Rest]) -> 158 | [Rule | optimize(Rest)]. 159 | 160 | %%-------------------------------------------------------------------- 161 | %% @doc 162 | %% Tests a span and performs the requested actions on it. 163 | %% @end 164 | %%-------------------------------------------------------------------- 165 | run(#span{tags = Tags, name = Name, duration = Duration}) -> 166 | ol_filter:check(Tags, Name, Duration). 167 | 168 | 169 | %% Since dialyzer will arn that the 'dummy'/empty implementation 170 | %% of ol_filter can't ever match send or cout we have to ignore 171 | %% this function 172 | -dialyzer({nowarn_function, perform/2}). 173 | perform([], _Span) -> 174 | ok; 175 | perform([send | Rest], Span) -> 176 | otters_conn_zipkin:store_span(Span), 177 | perform(Rest, Span); 178 | perform([{count, Path} | Rest], Span) -> 179 | otters_snapshot_count:snapshot(Path, Span), 180 | perform(Rest, Span). 181 | 182 | %%%=================================================================== 183 | %%% Compiler functions 184 | %%%=================================================================== 185 | group_rules([]) -> 186 | {ok, []}; 187 | 188 | group_rules([{Name, Test, Result} | Rest]) -> 189 | group_rules(Rest, Name, [{Test, Result}], []). 190 | 191 | group_rules([{Name, Test, Result} | Rest], Name, Conditions, Acc) -> 192 | group_rules(Rest, Name, [{Test, Result} | Conditions], Acc); 193 | group_rules([{Name, Test, Result} | Rest], LastName, Conditions, Acc) -> 194 | case lists:keyfind(Name, 1, Acc) of 195 | false -> 196 | Acc1 = [{LastName, optimize_conditions(Conditions)} | Acc], 197 | group_rules(Rest, Name, [{Test, Result}], Acc1); 198 | _ -> 199 | {error, {already_defined, Name}} 200 | end; 201 | group_rules([], LastName, Conditions, Acc) -> 202 | Acc1 = [{LastName, optimize_conditions(Conditions)} | Acc], 203 | {ok, optimize_rules(Acc1, [])}. 204 | 205 | %% Continue on the end of a condition makes no sense, there is nothing to 206 | %% continue to 207 | optimize_conditions([{_, continue} | R]) -> 208 | optimize_conditions(R); 209 | %% skip on the end of a condition makes no sense, there is nothing to skip 210 | optimize_conditions([{_, skip} | R]) -> 211 | optimize_conditions(R); 212 | optimize_conditions(R) -> 213 | lists:reverse(R). 214 | 215 | optimize_rules([{_, []} | R], Acc) -> 216 | optimize_rules(R, Acc); 217 | optimize_rules([E | R], Acc) -> 218 | optimize_rules(R, [E | Acc]); 219 | optimize_rules([], Acc) -> 220 | Acc. 221 | 222 | render([]) -> 223 | ["-module(ol_filter).\n", 224 | "-export([check/3]).\n", 225 | "-compile(inline).\n", 226 | "\n", 227 | "check(_Tags, _Name, _Duration) ->\n", 228 | " {ok, []}.\n", 229 | "\n"]; 230 | render([{Name,_} | _] = Cs) -> 231 | ["-module(ol_filter).\n", 232 | "-export([check/3]).\n", 233 | "-compile(inline).\n", 234 | "\n", 235 | "check(Tags, Name, Duration) ->\n", 236 | " ", rule_name(Name, 0), "(Tags, Name, Duration, []).\n", 237 | "\n", 238 | "get_tag(Key, Tags) ->\n", 239 | " KeyBin = otters_lib:to_bin(Key),\n", 240 | " case maps:find(KeyBin, Tags) of\n", 241 | " {ok, {V, _}} -> V;\n", 242 | " _ -> <<>>\n", 243 | " end.\n", 244 | "\n", 245 | render_(Cs)]. 246 | 247 | render_([{Name, Clauses} | [{NextName, _} | _] = R]) -> 248 | [render_clauses(Name, Clauses, NextName, 0), "\n", 249 | render_(R)]; 250 | render_([{Name, Clauses}]) -> 251 | [render_clauses(Name, Clauses, undefined, 0), "\n", 252 | "finish(_Tags, _Name, _Duration, Acc) ->\n", 253 | " {ok, Acc}.\n"]. 254 | 255 | rule_name(undefined, _N) -> 256 | "finish"; 257 | rule_name(Name, N) -> 258 | ["rule_", Name, "_", integer_to_list(N)]. 259 | 260 | render_clauses(_Name, [], _NextRule, _N) -> 261 | ""; 262 | 263 | render_clauses(Name, [{undefined, drop} | R], NextRule, N) -> 264 | [rule_name(Name, N), "(_Tags, _Name, _Duration, Acc) ->\n", 265 | " {ok, Acc}.\n\n", 266 | render_clauses(Name, R, NextRule, N + 1)]; 267 | 268 | %% Special case if we match for duration and name at once 269 | render_clauses(Name, [{{_Cmp, ?DURATION, _V}, continue} = A, 270 | {{_Cmp1, ?NAME, _V1}, continue} = B| R], 271 | NextRule, N) -> 272 | render_clauses(Name, [B, A| R], NextRule, N); 273 | 274 | render_clauses(Name, [{{Cmp, ?NAME, V}, continue}, 275 | {{Cmp1, ?DURATION, V1}, continue}| R], 276 | NextRule, N) -> 277 | {Body, R1} = continue_body(R, Name, N, NextRule), 278 | [rule_name(Name, N), 279 | io_lib:format("(Tags, Name, Duration, Acc) when Name ~s ~s," 280 | " Duration ~s ~s->\n", 281 | [Cmp, format_v(V), Cmp1, format_v(V1)]), 282 | " ", Body, ";\n", 283 | rule_name(Name, N), "(Tags, Name, Duration, Acc) ->\n", 284 | " ", rule_name(NextRule, 0), "(Tags, Name, Duration, Acc).\n\n", 285 | render_clauses(Name, R1, NextRule, N + 1)]; 286 | 287 | 288 | %% Speical cases for Duration 289 | render_clauses(Name, [{{exists, ?DURATION}, Action} | R], NextRule, N) -> 290 | [rule_name(Name, N), "(Tags, Name, Duration, Acc) ->\n", 291 | " ", render_action(Action, Name, N, NextRule, R), ".\n\n", 292 | render_clauses(Name, R, NextRule, N + 1)]; 293 | 294 | render_clauses(Name, [{{Cmp, ?DURATION, V}, continue} | R], 295 | NextRule, N) -> 296 | {Body, R1} = continue_body(R, Name, N, NextRule), 297 | [rule_name(Name, N), 298 | io_lib:format("(Tags, Name, Duration, Acc) when Duration ~s ~s ->\n", 299 | [Cmp, format_v(V)]), 300 | " ", Body, ";\n", 301 | rule_name(Name, N), "(Tags, Name, Duration, Acc) ->\n", 302 | " ", rule_name(NextRule, 0), "(Tags, Name, Duration, Acc).\n\n", 303 | render_clauses(Name, R1, NextRule, N + 1)]; 304 | 305 | render_clauses(Name, [{{Cmp, ?DURATION, V}, Action} | R], NextRule, N) -> 306 | [rule_name(Name, N), 307 | io_lib:format("(Tags, Name, Duration, Acc) " 308 | "when Duration ~s ~s ->\n", 309 | [Cmp, format_v(V)]), 310 | " ", render_action(Action, Name, N, NextRule, R), ";\n", 311 | rule_name(Name, N), "(Tags, Name, Duration, Acc) ->\n", 312 | " ", next_rule(Name, N, NextRule, R), 313 | render_clauses(Name, R, NextRule, N + 1)]; 314 | 315 | 316 | %% Speical cases for Name 317 | render_clauses(Name, [{{exists, ?NAME}, Action} | R], NextRule, N) -> 318 | [rule_name(Name, N), "(Tags, Name, Duration, Acc) ->\n", 319 | " ", render_action(Action, Name, N, NextRule, R), ".\n\n", 320 | render_clauses(Name, R, NextRule, N + 1)]; 321 | 322 | render_clauses(Name, [{{Cmp, ?NAME, V}, continue} | R], 323 | NextRule, N) -> 324 | {Body, R1} = continue_body(R, Name, N, NextRule), 325 | [rule_name(Name, N), 326 | io_lib:format("(Tags, Name, Duration, Acc) when Name ~s ~s ->\n", 327 | [Cmp, format_v(V)]), 328 | " ", Body, ";\n", 329 | rule_name(Name, N), "(Tags, Name, Duration, Acc) ->\n", 330 | " ", rule_name(NextRule, 0), "(Tags, Name, Duration, Acc).\n\n", 331 | render_clauses(Name, R1, NextRule, N + 1)]; 332 | 333 | render_clauses(Name, [{{Cmp, ?NAME, V}, Action} | R], NextRule, N) -> 334 | [rule_name(Name, N), 335 | io_lib:format("(Tags, Name, Duration, Acc) " 336 | "when Name ~s ~s ->\n", 337 | [Cmp, format_v(V)]), 338 | " ", render_action(Action, Name, N, NextRule, R), ";\n", 339 | rule_name(Name, N), "(Tags, Name, Duration, Acc) ->\n", 340 | " ", next_rule(Name, N, NextRule, R), 341 | render_clauses(Name, R, NextRule, N + 1)]; 342 | 343 | %% Normal cases 344 | render_clauses(Name, [{{exists, Key}, Action} | R], NextRule, N) -> 345 | [rule_name(Name, N), 346 | io_lib:format("(Tags = #{<<\"~s\">> := _}, Name, Duration, Acc) ->\n", 347 | [Key]), 348 | " ", render_action(Action, Name, N, NextRule, R), ";\n", 349 | rule_name(Name, N), "(Tags, Name, Duration, Acc) ->\n", 350 | " ", next_rule(Name, N, NextRule, R), 351 | render_clauses(Name, R, NextRule, N + 1)]; 352 | 353 | 354 | %% If we have mutliple continues in a row we can combine them, 355 | %% this combines two continues. 356 | 357 | render_clauses(Name, [{{Cmp1, Key1, V1}, continue}, 358 | {{Cmp2, Key2, V2}, continue}| R], NextRule, N) -> 359 | {Body, R1} = continue_body(R, Name, N, NextRule), 360 | [rule_name(Name, N), 361 | io_lib:format("(Tags = #{<<\"~s\">> := {_V1, _}," 362 | " <<\"~s\">> := {_V2, _}}, Name, Duration, Acc) " 363 | "when _V1 ~s ~s, " 364 | " _V2 ~s ~s ->\n", 365 | [Key1, Key2, Cmp1, format_v(V1), Cmp2, format_v(V2)]), 366 | " ", Body, ";\n", 367 | rule_name(Name, N), "(Tags, Name, Duration, Acc) ->\n", 368 | " ", next_rule(Name, N, NextRule, R1), 369 | render_clauses(Name, R1, NextRule, N + 1)]; 370 | 371 | render_clauses(Name, [{{Cmp, Key, V}, continue} | R], NextRule, N) -> 372 | {Body, R1} = continue_body(R, Name, N, NextRule), 373 | [rule_name(Name, N), 374 | io_lib:format("(Tags = #{<<\"~s\">> := {_V, _}}, Name, Duration, Acc) " 375 | "when _V ~s ~s ->\n", 376 | [Key, Cmp, format_v(V)]), 377 | " ", Body, ";\n", 378 | rule_name(Name, N), "(Tags, Name, Duration, Acc) ->\n", 379 | " ", next_rule(Name, N, NextRule, R1), 380 | render_clauses(Name, R1, NextRule, N + 1)]; 381 | 382 | render_clauses(Name, [{{Cmp, Key, V}, Action} | R], NextRule, N) -> 383 | [rule_name(Name, N), 384 | io_lib:format("(Tags = #{<<\"~s\">> := {_V, _}}, Name, Duration, Acc) " 385 | "when _V ~s ~s ->\n", 386 | [Key, Cmp, format_v(V)]), 387 | " ", render_action(Action, Name, N, NextRule, R), ";\n", 388 | rule_name(Name, N), "(Tags, Name, Duration, Acc) ->\n", 389 | " ", next_rule(Name, N, NextRule, R), 390 | render_clauses(Name, R, NextRule, N + 1)]; 391 | 392 | render_clauses(Name, [{undefined, Action} | R], NextRule, N) -> 393 | [rule_name(Name, N), "(Tags, Name, Duration, Acc) ->\n" 394 | " ", render_action(Action, Name, N, NextRule, R), ".\n\n", 395 | render_clauses(Name, R, NextRule, N + 1)]. 396 | 397 | render_action(drop, _Name, _N, _NextRule, _R) -> 398 | "{ok, Acc}"; 399 | render_action(skip, _Name, _N, NextRule, _R) -> 400 | [rule_name(NextRule, 0), "(Tags, Name, Duration, Acc)"]; 401 | 402 | render_action(send, Name, N, NextRule, R) -> 403 | [next_rule_name(Name, N, NextRule, R), "(Tags, Name, Duration, [send | Acc])"]; 404 | 405 | render_action({count, Path}, Name, N, NextRule, R) -> 406 | [next_rule_name(Name, N, NextRule, R), 407 | "(Tags, Name, Duration, [{count, [", make_path(Path), "]} | Acc])"]. 408 | 409 | 410 | next_rule(Name, N, NextRule, R) -> 411 | [next_rule_name(Name, N, NextRule, R), "(Tags, Name, Duration, Acc).\n\n"]. 412 | 413 | next_rule_name(_Name, _N, NextRule, []) -> 414 | rule_name(NextRule, 0); 415 | next_rule_name(Name, N, _NextRule, _R) -> 416 | rule_name(Name, N + 1). 417 | 418 | make_path([E]) -> 419 | make_e(E); 420 | make_path([E | R]) -> 421 | [make_e(E), ", ", make_path(R)]. 422 | 423 | 424 | make_e({get, ?NAME}) -> 425 | "Name"; 426 | make_e({get, ?DURATION}) -> 427 | "Duration"; 428 | make_e({get, V}) -> 429 | ["get_tag(", format_v(V), ", Tags)"]; 430 | make_e(V) -> 431 | format_v(V). 432 | 433 | format_v(V) when is_integer(V) -> 434 | integer_to_list(V); 435 | format_v(V) -> 436 | ["<<\"", V, "\">>"]. 437 | 438 | 439 | %% If a continue is followed by a always matching clause 440 | %% We also pull the clause in 441 | continue_body([{undefined, Action}| R], Name, N, NextRule) -> 442 | {render_action(Action, Name, N, NextRule, R), R}; 443 | continue_body(R, Name, N, _NextRule) -> 444 | {[rule_name(Name, N + 1), "(Tags, Name, Duration, Acc)"], R}. 445 | -------------------------------------------------------------------------------- /src/ol_filter.erl: -------------------------------------------------------------------------------- 1 | -module(ol_filter). 2 | 3 | -export([check/3]). 4 | 5 | check(_, _, _) -> 6 | {ok, []}. 7 | -------------------------------------------------------------------------------- /src/otters.app.src: -------------------------------------------------------------------------------- 1 | {application, otters, 2 | [{description, "OpenTracing Toolkit for Erlang"}, 3 | {vsn, git}, 4 | {registered, [otters_sup, otters_snapshot_count, otters_conn_zipkin]}, 5 | {mod, { otters_app, []}}, 6 | {applications, 7 | [kernel, 8 | stdlib, 9 | ibrowse, 10 | dynamic_compile 11 | ]}, 12 | {env,[ 13 | {http_client, ibrowse}, 14 | {zipkin_collector_uri, "http://127.0.0.1:9411/api/v1/spans"}, 15 | {zipkin_batch_interval_ms, 100}, 16 | {zipkin_tag_host_ip, {127,0,0,1}}, 17 | {zipkin_tag_host_port, 0}, 18 | {zipkin_tag_host_service, "otters"}, 19 | 20 | {zipkin_add_default_service_to_logs, false}, 21 | {zipkin_add_default_service_to_tags, false}, 22 | 23 | {zipkin_add_host_tag_to_span, {"lc", ""}} 24 | ]}, 25 | {modules, []}, 26 | {maintainers, ["Heinz N. Gies"]}, 27 | {licenses, ["Apache"]}, 28 | {links, [{"Github", "https://github.com/project-fifo/otters"}]} 29 | ]}. 30 | -------------------------------------------------------------------------------- /src/otters.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% Licensed to the Apache Software Foundation (ASF) under one 3 | %%% or more contributor license agreements. See the NOTICE file 4 | %%% distributed with this work for additional information 5 | %%% regarding copyright ownership. The ASF licenses this file 6 | %%% to you under the Apache License, Version 2.0 (the 7 | %%% "License"); you may not use this file except in compliance 8 | %%% with the License. You may obtain a copy of the License at 9 | %%% 10 | %%% http://www.apache.org/licenses/LICENSE-2.0 11 | %%% 12 | %%% Unless required by applicable law or agreed to in writing, 13 | %%% software distributed under the License is distributed on an 14 | %%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %%% KIND, either express or implied. See the License for the 16 | %%% specific language governing permissions and limitations 17 | %%% under the License. 18 | %%% 19 | %%% @doc 20 | %%% otters API module. Functions have no effect when 21 | %%% undefined is passed as a spawn. 22 | %%% 23 | %%% This API functions with passing around the Span in the function calls 24 | %%% All of them return a Span structure. 25 | %%% @end 26 | %%%------------------------------------------------------------------- 27 | 28 | -module(otters). 29 | -include("otters.hrl"). 30 | 31 | -export([start/1, start/2, start/3, 32 | start_child/2, 33 | tag/3, tag/4, 34 | log/2, log/3, 35 | finish/1, 36 | ids/1 37 | ]). 38 | 39 | -export_type([info/0, service/0, trace_id/0, span_id/0, 40 | span/0, maybe_span/0, tags/0]). 41 | 42 | -type info() :: binary() | iolist() | atom() | integer(). 43 | -type ip4() :: {0..255, 0..255, 0..255, 0..255}. 44 | -type service() :: binary() | list() | default | undefined | 45 | {binary() | list(), ip4(), integer()}. 46 | -type trace_id() :: non_neg_integer(). 47 | -type span_id() :: non_neg_integer(). 48 | -type tag() :: {info(), service()} 49 | | binary() | string() | atom(). 50 | -type tags() :: #{binary() => tag()}. 51 | -type span() :: #span{}. 52 | -type maybe_span() :: span() | undefined. 53 | 54 | %% timestamp in microseconds 55 | -type time_us() :: non_neg_integer(). 56 | 57 | 58 | %% ==================== SPAN function API ====================== 59 | 60 | %%-------------------------------------------------------------------- 61 | %% @doc 62 | %% Starts a new span with a given name and a generated trace id. 63 | %% @end 64 | %%-------------------------------------------------------------------- 65 | -spec start(info()) -> span(). 66 | start(Name) -> 67 | start(Name, otters_lib:id()). 68 | 69 | %%-------------------------------------------------------------------- 70 | %% @doc 71 | %% Starts a new span with a given Trace ID. 72 | %% @end 73 | %%-------------------------------------------------------------------- 74 | -spec start(info(), integer() | undefined) -> 75 | maybe_span(). 76 | start(_Name, undefined) -> 77 | undefined; 78 | start(Name, TraceId) 79 | when is_integer(TraceId) -> 80 | start(Name, TraceId, undefined). 81 | 82 | 83 | %%-------------------------------------------------------------------- 84 | %% @doc 85 | %% Starts a new span with a given Trace ID and Parent ID. 86 | %% @end 87 | %%-------------------------------------------------------------------- 88 | -spec start(info(), integer() | undefined, integer() | undefined) -> 89 | maybe_span(). 90 | start(_Name, undefined, undefined) -> 91 | undefined; 92 | start(Name, TraceId, ParentId) 93 | when is_integer(TraceId), (is_integer(ParentId) orelse 94 | ParentId =:= undefined) -> 95 | #span{ 96 | timestamp = otters_lib:timestamp(), 97 | trace_id = TraceId, 98 | id = otters_lib:id(), 99 | parent_id = ParentId, 100 | name = Name 101 | }. 102 | 103 | %%-------------------------------------------------------------------- 104 | %% @doc 105 | %% Starts a new span as a child of a existing span, using the parents 106 | %% Trace ID or a `{trace_id, parent_id}` tuple and setting the childs 107 | %% parent to the parents Span ID 108 | %% @end 109 | %%-------------------------------------------------------------------- 110 | -spec start_child(info(), maybe_span() | 111 | {TraceID::trace_id(), SpanID::span_id()}) -> maybe_span(). 112 | start_child(_Name, undefined) -> 113 | undefined; 114 | start_child(Name, #span{trace_id = TraceId, parent_id = ParentId}) -> 115 | start(Name, TraceId, ParentId); 116 | start_child(Name, {TraceId, ParentId}) -> 117 | start(Name, TraceId, ParentId). 118 | 119 | 120 | %%-------------------------------------------------------------------- 121 | %% @doc 122 | %% Adds a tag to a span, possibly overwriting the existing value. 123 | %% @end 124 | %%-------------------------------------------------------------------- 125 | -spec tag(maybe_span(), info(), info()) -> maybe_span(). 126 | tag(undefined, _Key, _Value) -> 127 | undefined; 128 | tag(Span = #span{}, Key, Value) -> 129 | tag(Span, Key, Value, undefined). 130 | 131 | 132 | %%-------------------------------------------------------------------- 133 | %% @doc 134 | %% Adds a tag to a span with a given service, possibly overwriting 135 | %% the existing value. 136 | %% @end 137 | %%-------------------------------------------------------------------- 138 | -spec tag(maybe_span(), info(), info(), service() | undefined) -> maybe_span(). 139 | tag(undefined, _Key, _Value, _Service) -> 140 | undefined; 141 | tag(Span= #span{}, Key, Value, Service) -> 142 | KeyBin = otters_lib:to_bin(Key), 143 | Span#span{ 144 | tags = maps:put(KeyBin, {v(Value), Service}, Span#span.tags) 145 | }. 146 | 147 | %%-------------------------------------------------------------------- 148 | %% @doc 149 | %% Adds a log to a span. 150 | %% @end 151 | %%-------------------------------------------------------------------- 152 | -spec log(maybe_span(), info()) -> maybe_span(). 153 | log(undefined, _Text) -> 154 | undefined; 155 | log(Span = #span{logs = Logs}, Text) -> 156 | Span#span{ 157 | logs = [{otters_lib:timestamp(), otters_lib:to_bin(Text), undefined} | Logs] 158 | }. 159 | 160 | %%-------------------------------------------------------------------- 161 | %% @doc 162 | %% Adds a log to a span with a given service. 163 | %% @end 164 | %%-------------------------------------------------------------------- 165 | -spec log(maybe_span(), info(), service()) -> maybe_span(). 166 | log(undefined, _Text, _Service) -> 167 | undefined; 168 | log(Span = #span{logs = Logs}, Text, Service) -> 169 | Span#span{ 170 | logs = [{otters_lib:timestamp(), otters_lib:to_bin(Text), Service} | Logs] 171 | }. 172 | 173 | %%-------------------------------------------------------------------- 174 | %% @doc 175 | %% Ends a span and prepares queues it to be dispatched to the trace 176 | %% server. This is also where filtering happens, it's the most 177 | %% expensive part of tracing. 178 | %% @end 179 | %%-------------------------------------------------------------------- 180 | -spec finish(maybe_span()) -> ok. 181 | finish(undefined) -> 182 | ok; 183 | finish(Span = #span{logs = Logs, timestamp = Start}) -> 184 | ol:span( 185 | Span#span{ 186 | duration = otters_lib:timestamp() - Start, 187 | logs = lists:reverse(Logs) 188 | }), 189 | ok. 190 | 191 | %%-------------------------------------------------------------------- 192 | %% @doc 193 | %% Retrives the Trace ID and the Span ID from a span. This can 194 | %% be used for start_child/2 195 | %% @end 196 | %%-------------------------------------------------------------------- 197 | -spec ids(maybe_span()) -> {TraceID::trace_id(), SpanID::span_id()} | undefined. 198 | ids(undefined) -> 199 | undefined; 200 | ids(#span{trace_id = TraceId, id = Id}) -> 201 | {TraceId, Id}. 202 | 203 | %%%=================================================================== 204 | %%% Internal functions 205 | %%%=================================================================== 206 | 207 | v(I) when is_integer(I) -> 208 | I; 209 | v(B) when is_binary(B) -> 210 | B; 211 | v(O) -> 212 | otters_lib:to_bin(O). 213 | -------------------------------------------------------------------------------- /src/otters_app.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% Licensed to the Apache Software Foundation (ASF) under one 3 | %%% or more contributor license agreements. See the NOTICE file 4 | %%% distributed with this work for additional information 5 | %%% regarding copyright ownership. The ASF licenses this file 6 | %%% to you under the Apache License, Version 2.0 (the 7 | %%% "License"); you may not use this file except in compliance 8 | %%% with the License. You may obtain a copy of the License at 9 | %%% 10 | %%% http://www.apache.org/licenses/LICENSE-2.0 11 | %%% 12 | %%% Unless required by applicable law or agreed to in writing, 13 | %%% software distributed under the License is distributed on an 14 | %%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %%% KIND, either express or implied. See the License for the 16 | %%% specific language governing permissions and limitations 17 | %%% under the License. 18 | %%% 19 | %%%------------------------------------------------------------------- 20 | 21 | -module(otters_app). 22 | 23 | -behaviour(application). 24 | 25 | %% Application callbacks 26 | -export([start/2, stop/1]). 27 | 28 | %%==================================================================== 29 | %% API 30 | %%==================================================================== 31 | 32 | start(_StartType, _StartArgs) -> 33 | case application:get_env(otters, filter_file, undefined) of 34 | undefined -> 35 | ok; 36 | F -> 37 | ok = ol:load(F) 38 | end, 39 | otters_sup:start_link(). 40 | 41 | %%-------------------------------------------------------------------- 42 | stop(_State) -> 43 | ok. 44 | 45 | %%==================================================================== 46 | %% Internal functions 47 | %%==================================================================== 48 | -------------------------------------------------------------------------------- /src/otters_config.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% Licensed to the Apache Software Foundation (ASF) under one 3 | %%% or more contributor license agreements. See the NOTICE file 4 | %%% distributed with this work for additional information 5 | %%% regarding copyright ownership. The ASF licenses this file 6 | %%% to you under the Apache License, Version 2.0 (the 7 | %%% "License"); you may not use this file except in compliance 8 | %%% with the License. You may obtain a copy of the License at 9 | %%% 10 | %%% http://www.apache.org/licenses/LICENSE-2.0 11 | %%% 12 | %%% Unless required by applicable law or agreed to in writing, 13 | %%% software distributed under the License is distributed on an 14 | %%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %%% KIND, either express or implied. See the License for the 16 | %%% specific language governing permissions and limitations 17 | %%% under the License. 18 | %%% 19 | %%% @end 20 | %%% ------------------------------------------------------------------- 21 | 22 | %% ========================== Config API ============================ 23 | %% The default implementation uses the application environment to 24 | %% store configuration. There is a simple wrapper module to interface 25 | %% with configuration store (otters_config). To implementat other config 26 | %% persistence, the module should be replaced with another one providing 27 | %% the same simple read/write API functions. 28 | %% WARNING : In the default implementation using the application 29 | %% environment, so the write function is NOT persistent. In case of node 30 | %% restart and/or application reload the configuration will be reset to 31 | %% whatever environment is defined in the release (sys) config or app 32 | %% file. There is an example configuration provided in the otters.app 33 | %% file as a reference. 34 | 35 | -module(otters_config). 36 | -export([ 37 | list/0, 38 | read/1, 39 | read/2, 40 | write/2 41 | ]). 42 | 43 | list() -> 44 | application:get_all_env(otters). 45 | 46 | read(Key) -> 47 | application:get_env(otters, Key). 48 | 49 | read(Key, Default) -> 50 | application:get_env(otters, Key, Default). 51 | 52 | %% This is provided to allow temporary configuration. Obviously in this 53 | %% default implementation it is not persistent as application environment 54 | %% from either the .app file in the ebin directory or from the release 55 | %% specific sys.config (or alike) will be read at startup. 56 | write(Key, Value) -> 57 | application:set_env(otters, Key, Value). 58 | -------------------------------------------------------------------------------- /src/otters_conn_zipkin.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% Licensed to the Apache Software Foundation (ASF) under one 3 | %%% or more contributor license agreements. See the NOTICE file 4 | %%% distributed with this work for additional information 5 | %%% regarding copyright ownership. The ASF licenses this file 6 | %%% to you under the Apache License, Version 2.0 (the 7 | %%% "License"); you may not use this file except in compliance 8 | %%% with the License. You may obtain a copy of the License at 9 | %%% 10 | %%% http://www.apache.org/licenses/LICENSE-2.0 11 | %%% 12 | %%% Unless required by applicable law or agreed to in writing, 13 | %%% software distributed under the License is distributed on an 14 | %%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %%% KIND, either express or implied. See the License for the 16 | %%% specific language governing permissions and limitations 17 | %%% under the License. 18 | %%% 19 | %%%------------------------------------------------------------------- 20 | 21 | %% @doc This module facilitates encoding/decoding of thrift data lists encoded 22 | %% with the binary protocol, suitable for sending/receiving spans 23 | %% on the zipkin interface. 24 | 25 | %% Sending is async 26 | 27 | -module(otters_conn_zipkin). 28 | -export([sup_init/0, store_span/1, send_buffer/0]). 29 | -include_lib("otters/include/otters.hrl"). 30 | 31 | -xref_ignore([encode_spans/1]). 32 | 33 | -ifdef(TEST). 34 | -compile(export_all). 35 | -endif. 36 | 37 | sup_init() -> 38 | [ 39 | ets:new( 40 | Tab, 41 | [named_table, public | TableProps ] 42 | ) || 43 | {Tab, TableProps} <- 44 | [ 45 | {otters_zipkin_buffer1, [{write_concurrency, true}, {keypos, 2}]}, 46 | {otters_zipkin_buffer2, [{write_concurrency, true}, {keypos, 2}]}, 47 | {otters_zipkin_status, [{read_concurrency, true}]} 48 | ] 49 | ], 50 | ets:insert(otters_zipkin_status, {current_buffer, otters_zipkin_buffer1}), 51 | SendInterval = otters_config:read(zipkin_batch_interval_ms, 100), 52 | timer:apply_interval(SendInterval, ?MODULE, send_buffer, []). 53 | 54 | store_span(Span) -> 55 | [{_, Buffer}] = ets:lookup(otters_zipkin_status, current_buffer), 56 | ets:insert(Buffer, Span). 57 | 58 | send_buffer() -> 59 | [{_, Buffer}] = ets:lookup(otters_zipkin_status, current_buffer), 60 | NewBuffer = case Buffer of 61 | otters_zipkin_buffer1 -> 62 | otters_zipkin_buffer2; 63 | otters_zipkin_buffer2 -> 64 | otters_zipkin_buffer1 65 | end, 66 | ets:insert(otters_zipkin_status, {current_buffer, NewBuffer}), 67 | case ets:tab2list(Buffer) of 68 | [] -> 69 | ok; 70 | Spans -> 71 | ets:delete_all_objects(Buffer), 72 | case send_batch_to_zipkin(Spans) of 73 | {ok, 202} -> 74 | otters_snapshot_count:snapshot( 75 | [?MODULE, send_buffer, ok], 76 | [{spans, length(Spans)}]); 77 | Error -> 78 | otters_snapshot_count:snapshot( 79 | [?MODULE, send_buffer, failed], 80 | [ 81 | {spans, length(Spans)}, 82 | {error, Error} 83 | ]) 84 | end 85 | end. 86 | 87 | send_batch_to_zipkin(Spans) -> 88 | {ok, ZipkinURL} = otters_config:read(zipkin_collector_uri), 89 | send_batch_to_zipkin(ZipkinURL, Spans). 90 | 91 | send_batch_to_zipkin(ZipkinURL, Spans) -> 92 | Data = otters_zipkin_encoder:encode(Spans), 93 | send_spans_http(ZipkinURL, Data). 94 | 95 | send_spans_http(ZipkinURL, Data) -> 96 | send_spans_http(application:get_env(otters, http_client, ibrowse), 97 | ZipkinURL, Data). 98 | 99 | send_spans_http(ibrowse, ZipkinURL, Data) -> 100 | case ibrowse:send_req( 101 | ZipkinURL, 102 | [{"content-type", "application/x-thrift"}], 103 | post, 104 | Data 105 | ) of 106 | {ok, SCode, _, _} -> 107 | {ok, list_to_integer(SCode)}; 108 | Err -> 109 | Err 110 | end; 111 | send_spans_http(httpc, ZipkinURL, Data) -> 112 | case httpc:request(post, {ZipkinURL, [], "application/x-thrift", Data}, 113 | [], []) of 114 | {ok, {{_, SCode, _}, _, _}} -> 115 | {ok, SCode}; 116 | Err -> 117 | Err 118 | end. 119 | 120 | -ifdef(TEST). 121 | encode_spans(Spans) -> 122 | Data = {struct, [span_to_struct(S) || S <- Spans]}, 123 | encode_implicit_list(Data). 124 | 125 | decode_spans(Data) -> 126 | {{struct, StructList}, _Rest} = decode_implicit_list(Data), 127 | [struct_to_span(S) || S <- StructList]. 128 | 129 | span_to_struct(#span{ 130 | id = Id, 131 | trace_id = TraceId, 132 | name = Name, 133 | parent_id = ParentId, 134 | logs = Logs, 135 | tags = TagsM, 136 | timestamp = Timestamp, 137 | duration = Duration 138 | }) -> 139 | Tags = maps:to_list(TagsM), 140 | FinalTags = 141 | case otters_config:read(zipkin_add_host_tag_to_span, undefined) of 142 | {Key, Value} -> 143 | [{Key, {Value, default}} | Tags]; 144 | _ -> 145 | Tags 146 | end, 147 | [ 148 | {1, i64, TraceId}, 149 | {3, string, otters_lib:to_bin(Name)}, 150 | {4, i64, Id} 151 | ] ++ 152 | case ParentId of 153 | undefined -> 154 | []; 155 | ParentId -> 156 | [{5, i64, ParentId}] 157 | end ++ 158 | [ 159 | {6, list, { 160 | struct, 161 | [log_to_annotation(Log) || Log <- Logs] 162 | }}, 163 | {8, list, { 164 | struct, 165 | [tag_to_binary_annotation(Tag) || Tag <- FinalTags] 166 | }}, 167 | {10, i64, Timestamp}, 168 | {11, i64, Duration} 169 | ]. 170 | 171 | log_to_annotation({Timestamp, Text, undefined}) -> 172 | case otters_config:read(zipkin_add_default_service_to_logs, false) of 173 | true -> 174 | log_to_annotation({Timestamp, Text, default}); 175 | false -> 176 | [ 177 | {1, i64, Timestamp}, 178 | {2, string, otters_lib:to_bin(Text)} 179 | ] 180 | end; 181 | log_to_annotation({Timestamp, Text, Service}) -> 182 | [ 183 | {1, i64, Timestamp}, 184 | {2, string, otters_lib:to_bin(Text)}, 185 | {3, struct, host_to_struct(Service)} 186 | ]. 187 | 188 | tag_to_binary_annotation({Key, {Value, undefined}}) -> 189 | case otters_config:read(zipkin_add_default_service_to_tags, false) of 190 | true -> 191 | tag_to_binary_annotation({Key, {Value, default}}); 192 | false -> 193 | [ 194 | {1, string, otters_lib:to_bin(Key)}, 195 | {2, string, otters_lib:to_bin(Value)}, 196 | {3, i32, 6} 197 | ] 198 | end; 199 | tag_to_binary_annotation({Key, {Value, Service}}) -> 200 | [ 201 | {1, string, otters_lib:to_bin(Key)}, 202 | {2, string, otters_lib:to_bin(Value)}, 203 | {3, i32, 6}, 204 | {4, struct, host_to_struct(Service)} 205 | ]. 206 | 207 | host_to_struct(default) -> 208 | DefaultService = otters_config:read( 209 | zipkin_tag_host_service, 210 | atom_to_list(node()) 211 | ), 212 | host_to_struct(DefaultService); 213 | host_to_struct(Service) 214 | when is_binary(Service); 215 | is_list(Service); 216 | is_atom(Service) -> 217 | host_to_struct({ 218 | otters_lib:to_bin(Service), 219 | otters_config:read(zipkin_tag_host_ip, {127, 0, 0, 1}), 220 | otters_config:read(zipkin_tag_host_port, 0) 221 | }); 222 | host_to_struct({Service, Ip, Port}) -> 223 | [ 224 | {1, i32, otters_lib:ip_to_i32(Ip)}, 225 | {2, i16, Port}, 226 | {3, string, Service} 227 | ]. 228 | 229 | struct_to_span(StructData) -> 230 | struct_to_span(StructData, #span{}). 231 | 232 | struct_to_span([{1, i64, TraceId}| Rest], Span) -> 233 | struct_to_span(Rest, Span#span{trace_id = TraceId}); 234 | struct_to_span([{3, string, Name}| Rest], Span) -> 235 | struct_to_span(Rest, Span#span{name = Name}); 236 | struct_to_span([{4, i64, Id}| Rest], Span) -> 237 | struct_to_span(Rest, Span#span{id = Id}); 238 | struct_to_span([{5, i64, ParentId}| Rest], Span) -> 239 | struct_to_span(Rest, Span#span{parent_id = ParentId}); 240 | struct_to_span([{6, list, {struct, Annotations}}| Rest], Span) -> 241 | Logs = [annotation_to_log(Annotation) || Annotation <- Annotations], 242 | struct_to_span(Rest, Span#span{logs = Logs}); 243 | struct_to_span([{8, list, {struct, BinAnnotations}}| Rest], Span) -> 244 | Tags = [bin_annotation_to_tag(BinAnnotation) || 245 | BinAnnotation <- BinAnnotations], 246 | struct_to_span(Rest, Span#span{tags = maps:from_list(Tags)}); 247 | struct_to_span([{10, i64, Timestamp}| Rest], Span) -> 248 | struct_to_span(Rest, Span#span{timestamp = Timestamp}); 249 | struct_to_span([{11, i64, Duration}| Rest], Span) -> 250 | struct_to_span(Rest, Span#span{duration = Duration}); 251 | struct_to_span([_ | Rest], Span) -> 252 | struct_to_span(Rest, Span); 253 | struct_to_span([], Span) -> 254 | Span. 255 | 256 | annotation_to_log(StructData) -> 257 | annotation_to_log(StructData, {undefined, undefined, undefined}). 258 | 259 | annotation_to_log([{1, i64, Timestamp} | Rest], {_, Text, Host}) -> 260 | annotation_to_log(Rest, {Timestamp, Text, Host}); 261 | annotation_to_log([{2, string, Text} | Rest], {Timestamp, _, Host}) -> 262 | annotation_to_log(Rest, {Timestamp, Text, Host}); 263 | annotation_to_log([{3, struct, HostStruct} | Rest], {Timestamp, Text, _}) -> 264 | annotation_to_log(Rest, {Timestamp, Text, struct_to_host(HostStruct)}); 265 | annotation_to_log([_ | Rest], Log) -> 266 | annotation_to_log(Rest, Log); 267 | annotation_to_log([], {Timestamp, Text, undefined}) -> 268 | {Timestamp, Text}; 269 | annotation_to_log([], Log) -> 270 | Log. 271 | 272 | bin_annotation_to_tag(StructData) -> 273 | bin_annotation_to_tag(StructData, {<<"">>, {<<"">>, undefined}}). 274 | 275 | -spec bin_annotation_to_tag( 276 | [term()], {binary(), {binary(), otters:service() | undefined}}) 277 | -> {binary(), 278 | {binary(), otters:service() | undefined}}. 279 | bin_annotation_to_tag([{1, string, Key} | Rest], {_, {Value, Host}}) -> 280 | bin_annotation_to_tag(Rest, {Key, {Value, Host}}); 281 | bin_annotation_to_tag([{2, string, Value} | Rest], {Key, {_, Host}}) -> 282 | bin_annotation_to_tag(Rest, {Key, {Value, Host}}); 283 | bin_annotation_to_tag([{4, struct, HostStruct} | Rest], {Key, {Value, _}}) -> 284 | bin_annotation_to_tag(Rest, {Key, {Value, struct_to_host(HostStruct)}}); 285 | bin_annotation_to_tag([_ | Rest], Tag) -> 286 | bin_annotation_to_tag(Rest, Tag); 287 | bin_annotation_to_tag([], Tag) -> 288 | Tag. 289 | 290 | struct_to_host(StructData) -> 291 | struct_to_host(StructData, {undefined, undefined, undefined}). 292 | 293 | struct_to_host([{1, i32, IntIp} | Rest], {Service, _, Port}) -> 294 | <> = <>, 295 | struct_to_host(Rest, {Service, {Ip1, Ip2, Ip3, Ip4}, Port}); 296 | struct_to_host([{2, i16, Port} | Rest], {Service, Ip, _}) -> 297 | struct_to_host(Rest, {Service, Ip, Port}); 298 | struct_to_host([{3, string, Service} | Rest], {_, Ip, Port}) -> 299 | struct_to_host(Rest, {Service, Ip, Port}); 300 | struct_to_host([_ | Rest], Host) -> 301 | struct_to_host(Rest, Host); 302 | struct_to_host([], Host) -> 303 | Host. 304 | 305 | %% encode/decode basic thrift binary data 306 | %% e.g. The transport (e.g. HTTP) data is an "implicit" list starting 307 | %% with the element type, and number of elements .. 308 | decode_implicit_list(BinaryData) -> 309 | decode(list, BinaryData). 310 | 311 | encode_implicit_list(Data) -> 312 | encode({list, Data}). 313 | 314 | %% Encoding functions 315 | encode({Id, Type, Data}) -> 316 | TypeId = map_type(Type), 317 | EData = encode({Type, Data}), 318 | <>; 319 | %% .. and without Id (i.e. part of list/set/map) 320 | encode({bool, true}) -> 321 | <<1>>; 322 | encode({bool, false}) -> 323 | <<0>>; 324 | encode({byte, Val}) -> 325 | <>; 326 | encode({double, Val}) -> 327 | <>; 328 | encode({i16, Val}) -> 329 | <>; 330 | encode({i32, Val}) -> 331 | <>; 332 | encode({i64, Val}) -> 333 | <>; 334 | encode({string, Val}) when is_list(Val) -> 335 | Size = length(Val), 336 | %% Might want to convert this to UTF-8 binary first, however for now 337 | %% I'll leave it to the next encoding when binary can be provided in 338 | %% UTF-8 format. In this part is kindly expected it to be ASCII 339 | %% string 340 | Bytes = list_to_binary(Val), 341 | <>; 342 | encode({string, Val}) when is_binary(Val) -> 343 | Size = byte_size(Val), 344 | <>; 345 | encode({list, {ElementType, Data}}) -> 346 | ElementTypeId = map_type(ElementType), 347 | Size = length(Data), 348 | EData = list_to_binary([ 349 | encode({ElementType, Element}) || 350 | Element <- Data 351 | ]), 352 | <>; 353 | encode({set, Data}) -> 354 | encode({list, Data}); 355 | encode({struct, Data}) -> 356 | EData = list_to_binary([ 357 | encode(StructElement) || 358 | StructElement <- Data 359 | ]), 360 | <>; 361 | encode({map, {KeyType, ValType, Data}}) -> 362 | KeyTypeId = map_type(KeyType), 363 | ValTypeId = map_type(ValType), 364 | Size = length(Data), 365 | EData = list_to_binary([ 366 | [encode({KeyType, Key}), encode({ValType, Val})] || 367 | {Key, Val} <- Data 368 | ]), 369 | <>. 370 | 371 | %% Decoding functions 372 | decode(<>) -> 373 | Type = map_type(TypeId), 374 | {Val, Rest} = decode(Type, Data), 375 | {{Id, Type, Val}, Rest}. 376 | 377 | decode(bool, <>) -> 378 | {Val == 1, Rest}; 379 | decode(byte, <>) -> 380 | {Val, Rest}; 381 | decode(double, <>) -> 382 | {Val, Rest}; 383 | decode(i16, <>) -> 384 | {Val, Rest}; 385 | decode(i32, <>) -> 386 | {Val, Rest}; 387 | decode(i64, <>) -> 388 | {Val, Rest}; 389 | decode(string, <>) -> 390 | <> = BytesAndRest, 391 | {Bytes, Rest}; 392 | decode(struct, Data) -> 393 | decode_struct(Data, []); 394 | decode(map, <>) -> 395 | decode_map( 396 | map_type(KeyTypeId), 397 | map_type(ValTypeId), 398 | Size, 399 | KVPsAndRest, 400 | [] 401 | ); 402 | %% Lists and Sets are encoded the same way 403 | decode(set, Data) -> 404 | decode(list, Data); 405 | decode(list, <>) -> 406 | decode_list( 407 | map_type(ElementTypeId), 408 | Size, 409 | ElementsAndRest, 410 | [] 411 | ). 412 | 413 | %% Helpers 414 | 415 | decode_struct(Data, Acc) -> 416 | case decode(Data) of 417 | {Val, <<0, Rest/bytes>>} -> 418 | {lists:reverse([Val | Acc]), Rest}; 419 | {Val, Rest} -> 420 | decode_struct(Rest, [Val | Acc]) 421 | end. 422 | 423 | decode_map(KeyType, ValType, 0, Rest, Acc) -> 424 | {{{KeyType, ValType}, lists:reverse(Acc)}, Rest}; 425 | decode_map(KeyType, ValType, Size, KVPsAndRest, Acc) -> 426 | {Key, ValAndRest} = decode(KeyType, KVPsAndRest), 427 | {Val, Rest} = decode(ValType, ValAndRest), 428 | decode_map(KeyType, ValType, Size-1, Rest, [{Key, Val} | Acc]). 429 | 430 | decode_list(ElementType, 0, Rest, Acc) -> 431 | {{ElementType, lists:reverse(Acc)}, Rest}; 432 | decode_list(ElementType, Size, Elements, Acc) -> 433 | {Data, Rest} = decode(ElementType, Elements), 434 | decode_list(ElementType, Size-1, Rest, [Data | Acc]). 435 | 436 | map_type(2) -> bool; 437 | map_type(3) -> byte; 438 | map_type(4) -> double; 439 | map_type(6) -> i16; 440 | map_type(8) -> i32; 441 | map_type(10) -> i64; 442 | map_type(11) -> string; 443 | map_type(12) -> struct; 444 | map_type(13) -> map; 445 | map_type(14) -> set; 446 | map_type(15) -> list; 447 | map_type(bool) -> 2; 448 | map_type(byte) -> 3; 449 | map_type(double)-> 4; 450 | map_type(i16) -> 6; 451 | map_type(i32) -> 8; 452 | map_type(i64) -> 10; 453 | map_type(string)-> 11; 454 | map_type(struct)-> 12; 455 | map_type(map) -> 13; 456 | map_type(set) -> 14; 457 | map_type(list) -> 15. 458 | -endif. 459 | -------------------------------------------------------------------------------- /src/otters_lib.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% Licensed to the Apache Software Foundation (ASF) under one 3 | %%% or more contributor license agreements. See the NOTICE file 4 | %%% distributed with this work for additional information 5 | %%% regarding copyright ownership. The ASF licenses this file 6 | %%% to you under the Apache License, Version 2.0 (the 7 | %%% "License"); you may not use this file except in compliance 8 | %%% with the License. You may obtain a copy of the License at 9 | %%% 10 | %%% http://www.apache.org/licenses/LICENSE-2.0 11 | %%% 12 | %%% Unless required by applicable law or agreed to in writing, 13 | %%% software distributed under the License is distributed on an 14 | %%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %%% KIND, either express or implied. See the License for the 16 | %%% specific language governing permissions and limitations 17 | %%% under the License. 18 | %%% 19 | %%%------------------------------------------------------------------- 20 | 21 | -module(otters_lib). 22 | -export([ 23 | id/0, 24 | ip_to_i32/1, 25 | i32_to_ip/1, 26 | timestamp/0, 27 | to_bin/1 28 | ]). 29 | 30 | 31 | ip_to_i32({A, B, C, D}) -> 32 | <> = <>, 33 | Ip. 34 | 35 | i32_to_ip(<>) -> 36 | {A, B, C, D}. 37 | 38 | timestamp() -> 39 | {MeS, S, MuS} = os:timestamp(), 40 | (MeS*1000000+S)*1000000+MuS. 41 | 42 | %% For some reason not all OSs allow for this :/ 43 | %% erlang:system_time(microsecond). 44 | 45 | id() -> 46 | <> = crypto:strong_rand_bytes(8), 47 | Id. 48 | 49 | to_bin(Int) when is_integer(Int)-> 50 | integer_to_binary(Int); 51 | to_bin(Atom) when is_atom(Atom) -> 52 | atom_to_binary(Atom, 'utf8'); 53 | to_bin(List) when is_list(List) -> 54 | unicode:characters_to_binary(List); 55 | to_bin(Binary) when is_binary(Binary) -> 56 | Binary; 57 | to_bin(Fun) when is_function(Fun, 0) -> 58 | to_bin(Fun()); 59 | to_bin(Value) -> 60 | unicode:characters_to_binary(io_lib:format("~1024p", [Value])). 61 | -------------------------------------------------------------------------------- /src/otters_snapshot_count.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% Licensed to the Apache Software Foundation (ASF) under one 3 | %%% or more contributor license agreements. See the NOTICE file 4 | %%% distributed with this work for additional information 5 | %%% regarding copyright ownership. The ASF licenses this file 6 | %%% to you under the Apache License, Version 2.0 (the 7 | %%% "License"); you may not use this file except in compliance 8 | %%% with the License. You may obtain a copy of the License at 9 | %%% 10 | %%% http://www.apache.org/licenses/LICENSE-2.0 11 | %%% 12 | %%% Unless required by applicable law or agreed to in writing, 13 | %%% software distributed under the License is distributed on an 14 | %%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %%% KIND, either express or implied. See the License for the 16 | %%% specific language governing permissions and limitations 17 | %%% under the License. 18 | %%% 19 | %%% @doc 20 | %%% This module implements a simple way of giving operational visibility 21 | %%% to events/spans in the system. It expects a Key and Data where the 22 | %%% Key is used to increment a counter in a table and also to store the 23 | %%% last received Data for that Key. The Data is preferred to be a list 24 | %%% of 2 element {K,V} tuples, if it is any other format, it uses 25 | %%% [{data, Data}] to store the last information. 26 | %%% @end 27 | %%%------------------------------------------------------------------- 28 | -module(otters_snapshot_count). 29 | -export([ 30 | delete_all_counters/0, 31 | delete_counter/1, 32 | get_snap/1, 33 | list_counts/0, 34 | snapshot/2, 35 | sup_init/0 36 | ]). 37 | 38 | 39 | snapshot(Key, [{_, _} |_ ] = Data) -> 40 | {_, _, Us} = os:timestamp(), 41 | {{Year, Month, Day}, {Hour, Min, Sec}} = calendar:local_time(), 42 | ets:insert( 43 | otters_snapshot_store, 44 | { 45 | Key, 46 | [ 47 | {snap_timestamp, {Year, Month, Day, Hour, Min, Sec, Us}} 48 | | Data 49 | ] 50 | } 51 | ), 52 | case catch ets:update_counter(otters_snapshot_count, Key, 1) of 53 | {'EXIT', {badarg, _}} -> 54 | ets:insert(otters_snapshot_count, {Key, 1}); 55 | Cnt -> 56 | Cnt 57 | end; 58 | snapshot(Key, Data) -> 59 | snapshot(Key, [{data, Data}]). 60 | 61 | list_counts() -> 62 | ets:tab2list(otters_snapshot_count). 63 | 64 | get_snap(Key) -> 65 | ets:lookup(otters_snapshot_store, Key). 66 | 67 | delete_counter(Key) -> 68 | ets:delete(otters_snapshot_store, Key), 69 | ets:delete(otters_snapshot_count, Key), 70 | ok. 71 | 72 | delete_all_counters() -> 73 | ets:delete_all_objects(otters_snapshot_store), 74 | ets:delete_all_objects(otters_snapshot_count), 75 | ok. 76 | 77 | sup_init() -> 78 | [ets:new( 79 | Tab, [named_table, public, {Concurrency, true}]) || 80 | {Tab, Concurrency} <- [{otters_snapshot_count, write_concurrency}, 81 | {otters_snapshot_store, write_concurrency}]]. 82 | -------------------------------------------------------------------------------- /src/otters_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% Licensed to the Apache Software Foundation (ASF) under one 3 | %%% or more contributor license agreements. See the NOTICE file 4 | %%% distributed with this work for additional information 5 | %%% regarding copyright ownership. The ASF licenses this file 6 | %%% to you under the Apache License, Version 2.0 (the 7 | %%% "License"); you may not use this file except in compliance 8 | %%% with the License. You may obtain a copy of the License at 9 | %%% 10 | %%% http://www.apache.org/licenses/LICENSE-2.0 11 | %%% 12 | %%% Unless required by applicable law or agreed to in writing, 13 | %%% software distributed under the License is distributed on an 14 | %%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %%% KIND, either express or implied. See the License for the 16 | %%% specific language governing permissions and limitations 17 | %%% under the License. 18 | %%% 19 | %%% @doc This module is used purely for test purposes. 20 | %%% @end 21 | %%%------------------------------------------------------------------- 22 | 23 | -module(otters_sup). 24 | -behaviour(supervisor). 25 | 26 | -export([start_link/0]). 27 | -export([init/1]). 28 | 29 | -define(SERVER, ?MODULE). 30 | 31 | start_link() -> 32 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 33 | 34 | init([]) -> 35 | otters_snapshot_count:sup_init(), 36 | otters_conn_zipkin:sup_init(), 37 | ChildSpecs = [], 38 | {ok, { {one_for_all, 0, 1}, ChildSpecs} }. 39 | -------------------------------------------------------------------------------- /src/otters_zipkin_encoder.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% Copyright (c) 2017 Heinz N. Gies 3 | %%% 4 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 5 | %%% of this software and associated documentation files (the "Software"), to 6 | %%% deal in the Software without restriction, including without limitation the 7 | %%% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 8 | %%% sell copies of the Software, and to permit persons to whom the Software is 9 | %%% furnished to do so, subject to the following conditions: 10 | %%% 11 | %%% The above copyright notice and this permission notice shall be included in 12 | %%% all copies or substantial portions of the Software.
13 | %%% 14 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING, 19 | %%% FROM OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | %%% IN THE SOFTWARE. 21 | %%% 22 | %%% @author Heinz N. Gies 23 | %%% @copyright (C) 2017, Heinz N. Gies 24 | %%% @doc High perofrmance encoder for the zapkin thrift protocol. 25 | %%% @end 26 | %%%------------------------------------------------------------------- 27 | 28 | -module(otters_zipkin_encoder). 29 | -export([encode/1]). 30 | -include("otters.hrl"). 31 | 32 | -ifdef(TEST). 33 | -compile(export_all). 34 | -endif. 35 | 36 | -define(T_I16, 6). 37 | -define(T_I32, 8). 38 | -define(T_I64, 10). 39 | -define(T_STRING, 11). 40 | -define(T_STRUCT, 12). 41 | -define(T_LIST, 15). 42 | 43 | 44 | -record(conf, 45 | { 46 | service :: binary(), 47 | ip :: non_neg_integer(), 48 | port :: non_neg_integer(), 49 | 50 | add_tag_to_log :: boolean(), 51 | add_tag_to_tag :: boolean(), 52 | add_tag :: undefined | {otters:info(), otters:info()}, 53 | server_bin_log_dflt = <<>> :: binary(), 54 | server_bin_tag_dflt = <<>> :: binary(), 55 | server_bin_log_undef = <<>> :: binary(), 56 | server_bin_tag_undef = <<>> :: binary() 57 | }). 58 | 59 | encode(#span{} = S) -> 60 | encode([S]); 61 | 62 | encode(Spans) when is_list(Spans) -> 63 | Size = length(Spans), 64 | Cfg = defaults(), 65 | <> 67 | || S <- Spans>>/binary>>. 68 | 69 | defaults() -> 70 | DefaultService = cfg(zipkin_tag_host_service, atom_to_binary(node(), utf8)), 71 | DfltIP = otters_lib:ip_to_i32(cfg(zipkin_tag_host_ip, {127, 0, 0, 1})), 72 | DfltPort = cfg(zipkin_tag_host_port, 0), 73 | 74 | AddDfltToLog = cfg(zipkin_add_default_service_to_logs, false), 75 | AddDfltToTag = cfg(zipkin_add_default_service_to_tags, false), 76 | C0 = #conf{ 77 | service = DefaultService, 78 | ip = DfltIP, 79 | port = DfltPort, 80 | 81 | add_tag_to_log = AddDfltToLog, 82 | add_tag_to_tag = AddDfltToTag, 83 | add_tag = cfg(zipkin_add_host_tag_to_span, undefined) 84 | }, 85 | 86 | HostBin = encode_host(default, C0), 87 | LogHostBin = <>, 88 | TagHostBin = <>, 89 | C0#conf{ 90 | 91 | server_bin_log_dflt = LogHostBin, 92 | server_bin_tag_dflt = TagHostBin, 93 | 94 | server_bin_log_undef = case AddDfltToLog of 95 | true -> 96 | LogHostBin; 97 | _ -> 98 | <<>> 99 | end, 100 | server_bin_tag_undef = case AddDfltToTag of 101 | true -> 102 | TagHostBin; 103 | _ -> 104 | <<>> 105 | end}. 106 | 107 | encode_span(#span{ 108 | id = Id, 109 | trace_id = TraceId, 110 | name = Name, 111 | parent_id = ParentId, 112 | logs = Logs, 113 | tags = Tags, 114 | timestamp = Timestamp, 115 | duration = Duration 116 | }, Cfg) -> 117 | {Tags0Bin, TagSize} 118 | = case Cfg#conf.add_tag of 119 | {Key, Value} -> 120 | {encode_tag({otters_lib:to_bin(Key), {Value, default}}, Cfg), 121 | maps:size(Tags) + 1}; 122 | _ -> 123 | {<<>>, maps:size(Tags)} 124 | end, 125 | LogSize = length(Logs), 126 | NameBin = otters_lib:to_bin(Name), 127 | ParentBin = case ParentId of 128 | undefined -> 129 | <<>>; 130 | ParentId -> 131 | <> 132 | end, 133 | << 134 | %% Header 135 | ?T_I64, 1:16, TraceId:64/signed-integer, 136 | ?T_STRING, 3:16, (byte_size(NameBin)):32, NameBin/binary, 137 | ?T_I64, 4:16, Id:64/signed-integer, 138 | ParentBin/binary, 139 | %% Logs 140 | ?T_LIST, 6:16, ?T_STRUCT, LogSize:32, 141 | (<< <<(encode_log(Log, Cfg))/binary>> || Log <- Logs >>)/bytes, 142 | %% Tags 143 | ?T_LIST, 8:16, ?T_STRUCT, TagSize:32, Tags0Bin/binary, 144 | (<< <<(encode_tag(Tag, Cfg))/binary>> 145 | || Tag <- maps:to_list(Tags)>>)/bytes, 146 | %% Tail 147 | ?T_I64, 10:16, Timestamp:64/signed-integer, 148 | ?T_I64, 11:16, Duration:64/signed-integer, 149 | 0>>. 150 | 151 | 152 | encode_host(default, Cfg) -> 153 | encode_host(Cfg#conf.service, Cfg); 154 | 155 | encode_host(Service, Cfg) 156 | when is_binary(Service) -> 157 | encode_host({Service, Cfg#conf.ip, Cfg#conf.port}, Cfg); 158 | 159 | encode_host(Service, Cfg) 160 | when is_list(Service); 161 | is_atom(Service) -> 162 | encode_host({ 163 | otters_lib:to_bin(Service), 164 | Cfg#conf.ip, 165 | Cfg#conf.port 166 | }, Cfg); 167 | encode_host({Service, Ip, Port}, _Cfg) when is_integer(Ip) -> 168 | <>; 172 | 173 | encode_host({Service, Ip, Port}, _Cfg) -> 174 | IPInt = otters_lib:ip_to_i32(Ip), 175 | <>. 179 | 180 | 181 | %% logs w/ undefined service 182 | 183 | %% binary text 184 | encode_log({Timestamp, Text, undefined}, Cfg) when is_binary(Text) -> 185 | <>; 189 | 190 | encode_log({Timestamp, Text, undefined}, Cfg) -> 191 | TextBin = otters_lib:to_bin(Text), 192 | <>; 196 | 197 | %% logs w/ undefined service 198 | 199 | %% binary text 200 | encode_log({Timestamp, Text, default}, Cfg) when is_binary(Text) -> 201 | <>; 205 | 206 | encode_log({Timestamp, Text, default}, Cfg) -> 207 | TextBin = otters_lib:to_bin(Text), 208 | <>; 212 | 213 | %% Other services 214 | 215 | encode_log({Timestamp, Text, Service}, Cfg) when is_binary(Text) -> 216 | SrvBin = encode_service(Service, log, Cfg), 217 | <>; 221 | 222 | encode_log({Timestamp, Text, Service}, Cfg) -> 223 | TextBin = otters_lib:to_bin(Text), 224 | SrvBin = encode_service(Service, log, Cfg), 225 | <>. 229 | 230 | %% Tags undefined 231 | encode_tag({Key, {Value, undefined}}, Cfg) 232 | when is_binary(Key), is_binary(Value) -> 233 | <>; 238 | 239 | encode_tag({Key, {Value, undefined}}, Cfg) 240 | when is_binary(Key) -> 241 | ValueBin = otters_lib:to_bin(Value), 242 | <>; 247 | 248 | %% TAgs default 249 | encode_tag({Key, {Value, default}}, Cfg) 250 | when is_binary(Key), is_binary(Value) -> 251 | <>; 256 | 257 | encode_tag({Key, {Value, default}}, Cfg) 258 | when is_binary(Key) -> 259 | ValueBin = otters_lib:to_bin(Value), 260 | <>; 265 | 266 | %% Other services 267 | encode_tag({Key, {Value, Service}}, Cfg) 268 | when is_binary(Key), is_binary(Value) -> 269 | SrvBin = encode_service(Service, tag, Cfg), 270 | <>; 275 | encode_tag({Key, {Value, Service}}, Cfg) 276 | when is_binary(Key) -> 277 | ValueBin = otters_lib:to_bin(Value), 278 | SrvBin = encode_service(Service, tag, Cfg), 279 | <>. 284 | 285 | encode_service(Service, log, Cfg) -> 286 | HostBin = encode_host(Service, Cfg), 287 | <>; 288 | encode_service(Service, tag, Cfg) -> 289 | HostBin = encode_host(Service, Cfg), 290 | <>. 291 | 292 | 293 | cfg(K, D) -> 294 | otters_config:read(K, D). 295 | -------------------------------------------------------------------------------- /src/ottersp.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% Licensed to the Apache Software Foundation (ASF) under one 3 | %%% or more contributor license agreements. See the NOTICE file 4 | %%% distributed with this work for additional information 5 | %%% regarding copyright ownership. The ASF licenses this file 6 | %%% to you under the Apache License, Version 2.0 (the 7 | %%% "License"); you may not use this file except in compliance 8 | %%% with the License. You may obtain a copy of the License at 9 | %%% 10 | %%% http://www.apache.org/licenses/LICENSE-2.0 11 | %%% 12 | %%% Unless required by applicable law or agreed to in writing, 13 | %%% software distributed under the License is distributed on an 14 | %%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %%% KIND, either express or implied. See the License for the 16 | %%% specific language governing permissions and limitations 17 | %%% under the License. 18 | %%% 19 | %%% @doc 20 | %%% This API uses the process dictionary to collect span information 21 | %%% and can be used when all span tags an events happen in the same 22 | %%% request handling process. 23 | %%% @end 24 | %%%------------------------------------------------------------------- 25 | 26 | -module(ottersp). 27 | 28 | -export([start/1, start/2, start/3, 29 | start_child/2, 30 | tag/2, tag/3, 31 | log/1, log/2, 32 | finish/0, 33 | ids/0, 34 | get_span/0, 35 | push/0, 36 | push/1, 37 | pop/0 38 | ]). 39 | 40 | -define(KEY, otters_span_information). 41 | -define(SKEY, otters_span_stack). 42 | 43 | %%-------------------------------------------------------------------- 44 | %% @doc 45 | %% Starts a new span with a given name and a generated trace id. 46 | %% @end 47 | %%-------------------------------------------------------------------- 48 | -spec start(otters:info()) -> ok. 49 | start(Name) -> 50 | put(?KEY, otters:start(Name)). 51 | 52 | %%-------------------------------------------------------------------- 53 | %% @doc 54 | %% Starts a new span with a given Trace ID. 55 | %% @end 56 | %%-------------------------------------------------------------------- 57 | -spec start(otters:info(), otters:trace_id()) -> ok. 58 | start(Name, TraceId) -> 59 | put(?KEY, otters:start(Name, TraceId)). 60 | 61 | %%-------------------------------------------------------------------- 62 | %% @doc 63 | %% Starts a new span with a given Trace ID and Parent ID. 64 | %% @end 65 | %%-------------------------------------------------------------------- 66 | -spec start(otters:info(), otters:trace_id(), otters:span_id()) -> ok. 67 | start(Name, TraceId, ParentId) -> 68 | put(?KEY, otters:start(Name, TraceId, ParentId)). 69 | 70 | %%-------------------------------------------------------------------- 71 | %% @doc 72 | %% Starts a new span as a child of a existing span, using the parents 73 | %% Trace ID and setting the childs parent to the parents Span ID 74 | %% @end 75 | %%-------------------------------------------------------------------- 76 | -spec start_child(otters:info(), otters:maybe_span()) -> otters:maybe_span(). 77 | start_child(Name, ParentSpan) -> 78 | put(?KEY, otters:start_child(Name, ParentSpan)). 79 | 80 | %%-------------------------------------------------------------------- 81 | %% @doc 82 | %% Adds a tag to the span, possibly overwriting the existing value. 83 | %% @end 84 | %%-------------------------------------------------------------------- 85 | -spec tag(otters:info(), otters:info()) -> ok. 86 | tag(Key, Value) -> 87 | Span = get(?KEY), 88 | put(?KEY, otters:tag(Span, Key, Value)), 89 | ok. 90 | 91 | %%-------------------------------------------------------------------- 92 | %% @doc 93 | %% Adds a tag to the span, possibly overwriting the existing value. 94 | %% @end 95 | %%-------------------------------------------------------------------- 96 | -spec tag(otters:info(), otters:info(), otters:service()) -> ok. 97 | tag(Key, Value, Service) -> 98 | Span = get(?KEY), 99 | put(?KEY, otters:tag(Span, Key, Value, Service)), 100 | ok. 101 | 102 | %%-------------------------------------------------------------------- 103 | %% @doc 104 | %% Adds a tag to the span, possibly overwriting the existing value. 105 | %% @end 106 | %%-------------------------------------------------------------------- 107 | -spec log(otters:info()) -> ok. 108 | log(Text) -> 109 | Span = get(?KEY), 110 | put(?KEY, otters:log(Span, Text)), 111 | ok. 112 | 113 | %%-------------------------------------------------------------------- 114 | %% @doc 115 | %% Adds the log to a span with a given service, possibly overwriting 116 | %% the existing value. 117 | %% @end 118 | %%-------------------------------------------------------------------- 119 | -spec log(otters:info(), otters:service()) -> ok. 120 | log(Text, Service) -> 121 | Span = get(?KEY), 122 | put(?KEY, otters:log(Span, Text, Service)), 123 | ok. 124 | 125 | %%-------------------------------------------------------------------- 126 | %% @doc 127 | %% Ends the span and prepares queues it to be dispatched to the trace 128 | %% server. 129 | %% @end 130 | %%-------------------------------------------------------------------- 131 | -spec finish() -> ok. 132 | finish() -> 133 | Span = get(?KEY), 134 | otters:finish(Span), 135 | put(?KEY, undefined). 136 | 137 | %% This call can be used to retrieve the IDs from the calling process 138 | %% e.g. when you have a gen_server and you get an API function call 139 | %% (which is in the context of the calling process) then calling pget_id() 140 | %% returns a {TraceId, SpanId} that is stored with the process API calls 141 | %% above for the calling process, so they can be used in the handling of 142 | %% the call 143 | 144 | %%-------------------------------------------------------------------- 145 | %% @doc 146 | %% Retrives the Trace ID and the Span ID from a span. 147 | %% @end 148 | %%-------------------------------------------------------------------- 149 | -spec ids() -> {otters:trace_id(), otters:span_id()} | undefined. 150 | ids() -> 151 | Span = get(?KEY), 152 | otters:ids(Span). 153 | 154 | %%-------------------------------------------------------------------- 155 | %% @doc 156 | %% Retreivers the current span 157 | %% @end 158 | %%-------------------------------------------------------------------- 159 | -spec get_span() -> otters:maybe_span(). 160 | get_span() -> 161 | get(?KEY). 162 | 163 | %%-------------------------------------------------------------------- 164 | %% @doc 165 | %% Pushes the current span on the span stack and replaces it with an 166 | %% emtpy/noop span. 167 | %% @end 168 | %%-------------------------------------------------------------------- 169 | 170 | -spec push() -> otters:maybe_span(). 171 | push() -> 172 | push(undefined). 173 | 174 | %%-------------------------------------------------------------------- 175 | %% @doc 176 | %% Pushes the current span on the span stack and replaces it the 177 | %% provided span. 178 | %% @end 179 | %%-------------------------------------------------------------------- 180 | 181 | -spec push(otters:maybe_span()) -> otters:maybe_span(). 182 | push(NewSpan) -> 183 | Span = get(?KEY), 184 | put(?KEY, NewSpan), 185 | case get(?SKEY) of 186 | undefined -> 187 | put(?SKEY, [Span]); 188 | Spans -> 189 | put(?SKEY, [Span | Spans]) 190 | end, 191 | Span. 192 | 193 | %%-------------------------------------------------------------------- 194 | %% @doc 195 | %% Pops the head of the span stack and returns it. If the stack 196 | %% is emtpy 'undefined' / noop is reutrn. poping more often then 197 | %% pushing does no harm. 198 | %% @end 199 | %%-------------------------------------------------------------------- 200 | -spec pop() -> otters:maybe_span(). 201 | pop() -> 202 | OldSpan = get(?KEY), 203 | NewSpan = case get(?SKEY) of 204 | undefined -> 205 | undefined; 206 | [] -> 207 | undefined; 208 | [Span | Spans] -> 209 | put(?SKEY, Spans), 210 | Span 211 | end, 212 | put(?KEY, NewSpan), 213 | OldSpan. 214 | -------------------------------------------------------------------------------- /test/bench_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% Copyright (c) 2017 Heinz N. Gies 3 | %%% 4 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 5 | %%% of this software and associated documentation files (the "Software"), to 6 | %%% deal in the Software without restriction, including without limitation the 7 | %%% rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 8 | %%% sell copies of the Software, and to permit persons to whom the Software is 9 | %%% furnished to do so, subject to the following conditions: 10 | %%% 11 | %%% The above copyright notice and this permission notice shall be included in 12 | %%% all copies or substantial portions of the Software.
13 | %%% 14 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING, 19 | %%% FROM OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | %%% IN THE SOFTWARE. 21 | %%% 22 | %%% @author Heinz N. Gies 23 | %%% @copyright (C) 2017, Heinz N. Gies 24 | %%% @doc High perofrmance encoder for the zapkin thrift protocol. 25 | %%% @end 26 | %%%------------------------------------------------------------------- 27 | 28 | -module(bench_SUITE). 29 | -include_lib("common_test/include/ct.hrl"). 30 | -include_lib("eunit/include/eunit.hrl"). 31 | -include_lib("otters/include/otters.hrl"). 32 | 33 | -export([all/0, 34 | bench_old_filter/1, bench_new_filter/1, 35 | bench_old_filter_large/1, bench_new_filter_large/1, 36 | bench_encoding/1]). 37 | 38 | all() -> 39 | [ 40 | bench_encoding, 41 | bench_old_filter, bench_new_filter, 42 | bench_old_filter_large, bench_new_filter_large 43 | ]. 44 | 45 | bench_old_filter(_) -> 46 | %% Set the filter 47 | %%application:ensure_all_started(otters), 48 | Filter = [ 49 | { 50 | %% Condition 51 | [ 52 | {greater, <<"otters_span_duration">>, 5000000}, 53 | {value, <<"otters_span_name">>, <<"radius request">>} 54 | ], 55 | %% Action 56 | [ 57 | {snapshot_count, [long_radius_request], []}, 58 | send_to_zipkin 59 | ] 60 | }, 61 | 62 | { 63 | %% Condition counts all requests with name and result 64 | [{present, <<"final_result">>}], 65 | %% Action 66 | [{snapshot_count, [request], 67 | [<<"otters_span_name">>, <<"final_result">>]}] 68 | } 69 | ], 70 | otters_config:write(filter_rules, Filter), 71 | ol:clear(), 72 | run(fun run_spans_f/1). 73 | 74 | bench_old_filter_large(_) -> 75 | %% Set the filter 76 | %%application:ensure_all_started(otters), 77 | Filter = [ 78 | { 79 | %% Condition 80 | [ 81 | {greater, <<"otters_span_duration">>, 5000000}, 82 | {value, <<"otters_span_name">>, <<"radius request">>} 83 | ], 84 | %% Action 85 | [ 86 | {snapshot_count, [long_radius_request], []}, 87 | send_to_zipkin 88 | ] 89 | }, 90 | {[{greater, <<"otters_span_duration">>, 5000000}, 91 | {value, <<"otters_span_name">>, <<"radius request">>}], 92 | [{snapshot_count, [<<"i">>], []}]}, 93 | {[{greater, <<"otters_span_duration">>, 5000000}, 94 | {value, <<"otters_span_name">>, <<"radius request">>}], 95 | [{snapshot_count, [<<"i">>], []}]}, 96 | {[{greater, <<"otters_span_duration">>, 5000000}, 97 | {value, <<"otters_span_name">>, <<"radius request">>}], 98 | [{snapshot_count, [<<"i">>], []}]}, 99 | {[{greater, <<"otters_span_duration">>, 5000000}, 100 | {value, <<"otters_span_name">>, <<"radius request">>}], 101 | [{snapshot_count, [<<"i">>], []}]}, 102 | {[{greater, <<"otters_span_duration">>, 5000000}, 103 | {value, <<"otters_span_name">>, <<"radius request">>}], 104 | [{snapshot_count, [<<"i">>], []}]}, 105 | {[{greater, <<"otters_span_duration">>, 5000000}, 106 | {value, <<"otters_span_name">>, <<"radius request">>}], 107 | [{snapshot_count, [<<"i">>], []}]}, 108 | {[{greater, <<"otters_span_duration">>, 5000000}, 109 | {value, <<"otters_span_name">>, <<"radius request">>}], 110 | [{snapshot_count, [<<"i">>], []}]}, 111 | {[{greater, <<"otters_span_duration">>, 5000000}, 112 | {value, <<"otters_span_name">>, <<"radius request">>}], 113 | [{snapshot_count, [<<"i">>], []}]}, 114 | {[{greater, <<"otters_span_duration">>, 5000000}, 115 | {value, <<"otters_span_name">>, <<"radius request">>}], 116 | [{snapshot_count, [<<"i">>], []}]}, 117 | 118 | { 119 | %% Condition counts all requests with name and result 120 | [{present, <<"final_result">>}], 121 | %% Action 122 | [{snapshot_count, [request], 123 | [<<"otters_span_name">>, <<"final_result">>]}] 124 | } 125 | ], 126 | otters_config:write(filter_rules, Filter), 127 | ol:clear(), 128 | run(fun run_spans_f/1). 129 | 130 | bench_new_filter_large(_) -> 131 | %% Set the filter 132 | %%application:ensure_all_started(otters), 133 | Filter = "%% If our span takes less then 5s skip the rest of the rules\n" 134 | "slow_spans(otters_span_duration > 5000000) ->\n" 135 | " continue.\n" 136 | "%% Skip requests that are not radius requests\n" 137 | "slow_spans(otters_span_name == 'radius request') ->\n" 138 | " continue.\n" 139 | "%% Count\n" 140 | "slow_spans() ->\n" 141 | " count('long_radius_request').\n" 142 | "%% Send\n" 143 | "slow_spans() ->\n" 144 | " send.\n" 145 | 146 | "slow_spans1(otters_span_duration > 5000000) ->\n" 147 | " continue.\n" 148 | "%% Skip requests that are not radius requests\n" 149 | "slow_spans1(otters_span_name == 'radius request') ->\n" 150 | " continue.\n" 151 | "%% Count\n" 152 | "slow_spans1() ->\n" 153 | " count('i').\n" 154 | 155 | "slow_spans2(otters_span_duration > 5000000) ->\n" 156 | " continue.\n" 157 | "%% Skip requests that are not radius requests\n" 158 | "slow_spans2(otters_span_name == 'radius request') ->\n" 159 | " continue.\n" 160 | "%% Count\n" 161 | "slow_spans2() ->\n" 162 | " count('i').\n" 163 | 164 | "slow_spans3(otters_span_duration > 5000000) ->\n" 165 | " continue.\n" 166 | "%% Skip requests that are not radius requests\n" 167 | "slow_spans3(otters_span_name == 'radius request') ->\n" 168 | " continue.\n" 169 | "%% Count\n" 170 | "slow_spans3() ->\n" 171 | " count('i').\n" 172 | 173 | "slow_spans4(otters_span_duration > 5000000) ->\n" 174 | " continue.\n" 175 | "%% Skip requests that are not radius requests\n" 176 | "slow_spans4(otters_span_name == 'radius request') ->\n" 177 | " continue.\n" 178 | "%% Count\n" 179 | "slow_spans4() ->\n" 180 | " count('i').\n" 181 | 182 | "slow_spans5(otters_span_duration > 5000000) ->\n" 183 | " continue.\n" 184 | "%% Skip requests that are not radius requests\n" 185 | "slow_spans5(otters_span_name == 'radius request') ->\n" 186 | " continue.\n" 187 | "%% Count\n" 188 | "slow_spans5() ->\n" 189 | " count('i').\n" 190 | 191 | "slow_spans6(otters_span_duration > 5000000) ->\n" 192 | " continue.\n" 193 | "%% Skip requests that are not radius requests\n" 194 | "slow_spans6(otters_span_name == 'radius request') ->\n" 195 | " continue.\n" 196 | "%% Count\n" 197 | "slow_spans6() ->\n" 198 | " count('i').\n" 199 | 200 | "slow_spans7(otters_span_duration > 5000000) ->\n" 201 | " continue.\n" 202 | "%% Skip requests that are not radius requests\n" 203 | "slow_spans7(otters_span_name == 'radius request') ->\n" 204 | " continue.\n" 205 | "%% Count\n" 206 | "slow_spans7() ->\n" 207 | " count('i').\n" 208 | 209 | "slow_spans8(otters_span_duration > 5000000) ->\n" 210 | " continue.\n" 211 | "%% Skip requests that are not radius requests\n" 212 | "slow_spans8(otters_span_name == 'radius request') ->\n" 213 | " continue.\n" 214 | "%% Count\n" 215 | "slow_spans8() ->\n" 216 | " count('i').\n" 217 | 218 | "slow_spans9(otters_span_duration > 5000000) ->\n" 219 | " continue.\n" 220 | "%% Skip requests that are not radius requests\n" 221 | "slow_spans9(otters_span_name == 'radius request') ->\n" 222 | " continue.\n" 223 | "%% Count\n" 224 | "slow_spans9() ->\n" 225 | " count('i').\n" 226 | 227 | "%% Count them all\n" 228 | "count(final_result) ->\n" 229 | " count('request', otter_span_name, final_result).\n" , 230 | ol:compile(Filter), 231 | run(fun run_spans/1). 232 | 233 | bench_new_filter(_) -> 234 | %% Set the filter 235 | %%application:ensure_all_started(otters), 236 | Filter = "%% If our span takes less then 5s skip the rest of the rules\n" 237 | "slow_spans(otters_span_duration > 5000000) ->\n" 238 | " continue.\n" 239 | "%% Skip requests that are not radius requests\n" 240 | "slow_spans(otters_span_name == 'radius request') ->\n" 241 | " continue.\n" 242 | "%% Count\n" 243 | "slow_spans() ->\n" 244 | " count('long_radius_request').\n" 245 | "%% Send\n" 246 | "slow_spans() ->\n" 247 | " send.\n" 248 | "%% Count them all\n" 249 | "count(final_result) ->\n" 250 | " count('request', otter_span_name, final_result).\n" , 251 | ol:compile(Filter), 252 | run(fun run_spans/1). 253 | 254 | run(Fn) -> 255 | {module, otters_conn_zipkin} = 256 | dynamic_compile:load_from_string( 257 | "-module(otters_conn_zipkin).\n" 258 | "-export([store_span/1]).\n" 259 | "store_span(_) -> ok.\n"), 260 | {module, otters_snapshot_count} = 261 | dynamic_compile:load_from_string( 262 | "-module(otters_snapshot_count).\n" 263 | "-export([snapshot/2]).\n" 264 | "snapshot(_, _) -> ok.\n"), 265 | Count = 100000, 266 | Spans = mk_spans(), 267 | Seq = lists:seq(1, Count), 268 | {T, _} = timer:tc(fun () -> 269 | lists:foreach(fun(_) -> 270 | Fn(Spans) 271 | end, Seq) 272 | end), 273 | io:format(user, "~.2f microseconds / span.~n", 274 | [T / (Count * length(Spans))]), 275 | %% We log 2 out of 3 messages so we need to multiply this 276 | %% by 2 277 | %%?assertEqual(2 * Count, LogCount), 278 | %% For long count we only have one of the spans 279 | %% matching so we count then only once 280 | %%?assertEqual(Count, LogLongCount), 281 | %%?assertEqual(Count, SendCount). 282 | ok. 283 | 284 | %%% Original 285 | %%% Encoding: 422.18 microseconds / span. 286 | %%% Decoding: 140.12 microseconds / span. 287 | %%% bench_SUITE ==> bench_encoding: OK 288 | %%% New 289 | %%% Encoding: 9.73 microseconds / span. 290 | %%% Decoding: 127.38 microseconds / span. 291 | %%% bench_SUITE ==> bench_encoding: OK 292 | bench_encoding(_) -> 293 | Count = 10000, 294 | Spans = mk_spans(), 295 | Spans1 = [Spans || _ <- lists:seq(1, Count)], 296 | Spans2 = lists:flatten(Spans1), 297 | Total = length(Spans2), 298 | {To, _Encoded} = timer:tc(otters_conn_zipkin, encode_spans, [Spans2]), 299 | {Te, Encoded} = timer:tc(otters_zipkin_encoder, encode, [Spans2]), 300 | 301 | io:format(user, "Encoding: ~.2f microseconds / span.~n", 302 | [Te / Total]), 303 | io:format(user, "Encoding(Old): ~.2f microseconds / span.~n", 304 | [To / Total]), 305 | {Td, _} = timer:tc(otters_conn_zipkin, decode_spans, [Encoded]), 306 | io:format(user, "Decoding: ~.2f microseconds / span.~n", 307 | [Td / Total]), 308 | ok. 309 | 310 | c_l(N) -> 311 | receive 312 | {get, P} -> 313 | P ! {n, N}; 314 | inc -> 315 | c_l(N + 1) 316 | end. 317 | 318 | run_spans_f([]) -> 319 | ok; 320 | run_spans_f([S | R]) -> 321 | otters_filter:span(S), 322 | run_spans_f(R). 323 | 324 | run_spans([]) -> 325 | ok; 326 | run_spans([S | R]) -> 327 | ol:span(S), 328 | run_spans(R). 329 | 330 | mk_spans() -> 331 | S = mk_span(), 332 | Tags = S#span.tags, 333 | Tags1 = Tags#{<<"final_result">> => {<<"yay!">>, undefined}}, 334 | [ 335 | %% Matches no rules 336 | S, 337 | %% Matches the count rule 338 | S#span{tags = Tags1}, 339 | %% Matches both rules 340 | S#span{duration = 5000001, name = <<"radius request">>, tags = Tags1} 341 | ]. 342 | %%[S]. 343 | 344 | 345 | mk_span() -> 346 | #span{ 347 | id = 0, 348 | timestamp = 0, 349 | trace_id = 0, 350 | duration = 100, 351 | name = <<"other request">>, 352 | tags = #{ 353 | <<"1">> => {1, undefined}, 354 | <<"2">> => {<<"hello">>, undefined}, 355 | <<"3">> => {1, default}, 356 | <<"4">> => {1, undefined}, 357 | <<"5">> => {<<"hello">>, default}, 358 | <<"6">> => {1, {<<"test">>, {127, 0, 0, 1}, 1}}, 359 | <<"7">> => {1, undefined}, 360 | <<"8">> => {<<"hello">>, undefined}, 361 | <<"9">> => {1, default}, 362 | <<"10">> => {1, {<<"test">>, {127, 0, 0, 1}, 1}} 363 | }, 364 | logs = [ 365 | {1, <<"test">>, default}, 366 | {2, <<"bla">>, undefined}, 367 | {3, <<"blubber">>, default}, 368 | {4, <<"hello">>, {<<"test">>, {127, 0, 0, 1}, 1}}, 369 | {5, <<"world">>, undefined} 370 | ] 371 | }. 372 | -------------------------------------------------------------------------------- /test/otter_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(otter_SUITE). 2 | -include_lib("common_test/include/ct.hrl"). 3 | -include_lib("eunit/include/eunit.hrl"). 4 | 5 | -export([all/0, ptest/1, ftest/1, ptest_push/1]). 6 | 7 | all() -> 8 | [ftest, ptest, ptest_push]. 9 | 10 | ptest(_Config) -> 11 | application:ensure_all_started(otters), 12 | application:set_env(otters, zipkin_collector_uri, 13 | "http://127.0.0.1:19411/api/v1/spans"), 14 | 15 | meck:new(ibrowse, [passthrough]), 16 | meck:expect(ibrowse, send_req, fun send_req/4), 17 | 18 | ottersp:start("test_span"), 19 | ottersp:log("started"), 20 | ottersp:log("α =:= ω"), 21 | ottersp:tag("result", "ok"), 22 | ottersp:finish(), 23 | timer:sleep(200), 24 | ?assert(meck:validate(ibrowse)), 25 | meck:unload(ibrowse). 26 | 27 | ptest_push(_Config) -> 28 | application:ensure_all_started(otters), 29 | application:set_env(otters, zipkin_collector_uri, 30 | "http://127.0.0.1:19411/api/v1/spans"), 31 | 32 | meck:new(ibrowse, [passthrough]), 33 | meck:expect(ibrowse, send_req, fun send_req/4), 34 | 35 | ottersp:start("test_span"), 36 | ottersp:log("started"), 37 | ottersp:log("α =:= ω"), 38 | ottersp:tag("result", "ok"), 39 | ottersp:push(), 40 | ?assertEqual(undefined, ottersp:get_span()), 41 | ottersp:pop(), 42 | ottersp:finish(), 43 | timer:sleep(200), 44 | ?assert(meck:validate(ibrowse)), 45 | meck:unload(ibrowse). 46 | 47 | ftest(_Config) -> 48 | application:ensure_all_started(otters), 49 | application:set_env(otters, zipkin_collector_uri, 50 | "http://127.0.0.1:19411/api/v1/spans"), 51 | application:set_env(otters, server_zipkin_callback, {?MODULE, handle_span}), 52 | ets:new(test_span_collector, [named_table, public, {keypos, 2}]), 53 | 54 | meck:new(ibrowse, [passthrough]), 55 | meck:expect(ibrowse, send_req, fun send_req/4), 56 | 57 | S1 = otters:start("test_span"), 58 | S2 = otters:log(S1, "started"), 59 | S3 = otters:tag(S2, "result", "ok"), 60 | S4 = otters:log(S3, "α =:= ω"), 61 | S5 = otters:log(S4, 123456), 62 | S6 = otters:log(S5, 'this is a atom'), 63 | S7 = otters:log(S6, 64 | io_lib:format("int: ~w, float: ~f, hex: ~.16B, Span: ~p", 65 | [1, 1.0, 1, S6])), 66 | S8 = otters:log(S7, S7), 67 | S9 = otters:log(S8, fun() -> "result of function" end), 68 | otters:finish(S9), 69 | timer:sleep(200), 70 | ?assert(meck:validate(ibrowse)), 71 | meck:unload(ibrowse). 72 | 73 | 74 | send_req(_ZipkinURL, 75 | [{"content-type", "application/x-thrift"}], 76 | post, Data) -> 77 | 1 = length(otters_conn_zipkin:decode_spans(Data)), 78 | {ok, {{stuff, 200, stuff}, stuff, stuff}}. 79 | 80 | handle_span(Span) -> 81 | ets:insert(test_collector, Span). 82 | -------------------------------------------------------------------------------- /test/otters_filter.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% Licensed to the Apache Software Foundation (ASF) under one 3 | %%% or more contributor license agreements. See the NOTICE file 4 | %%% distributed with this work for additional information 5 | %%% regarding copyright ownership. The ASF licenses this file 6 | %%% to you under the Apache License, Version 2.0 (the 7 | %%% "License"); you may not use this file except in compliance 8 | %%% with the License. You may obtain a copy of the License at 9 | %%% 10 | %%% http://www.apache.org/licenses/LICENSE-2.0 11 | %%% 12 | %%% Unless required by applicable law or agreed to in writing, 13 | %%% software distributed under the License is distributed on an 14 | %%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | %%% KIND, either express or implied. See the License for the 16 | %%% specific language governing permissions and limitations 17 | %%% under the License. 18 | %%% 19 | %%% 20 | %%% @doc 21 | %%% The main idea behind this filter that the processing of the spans can 22 | %%% be modified runtime by changing the filter configuration. This way 23 | %%% logging, counting or sending data to trace collectors can be modified 24 | %%% on the running system based on the changing operational requirements. 25 | %%% 26 | %%% The span filter works with key value pair lists. This was the 27 | %%% easiest to implement and reasonably fast. 28 | %%% 29 | %%% Filter rules are composed by a list of `{Conditions, Actions}' tuples. 30 | %%% Processing a span means iterating through this list and when an item 31 | %%% found where all `Conditions' evaluate to true then the `Actions' in that 32 | %%% item are executed. 33 | %%% 34 | %%% `Conditions' operate on a copy of the tags of the span. It is a 35 | %%% sequence of checks against the tags (e.g. key present, key value) 36 | %%% where if any check in the sequence fails, the associated actions are 37 | %%% not executed and the next `{Conditions, Actions}' item is evaluated. 38 | %%% `Actions' can trigger e.g. counting a particular tag value combination, 39 | %%% logging, sending the span to trace collectors, modifying the working 40 | %%% (i.e. used for further conditions) tag list, modifying the span tag 41 | %%% list that is to be sent to trace collector. `Actions' can also 42 | %%% influence the further evaluation of the rules i.e. providing the 43 | %%% break action instructs the rule engine NOT to look further in the 44 | %%% list of `{Conditions, Actions}'. 45 | %%% 46 | %%% `Evaluation' of the rules happens in the process which invokes the span 47 | %%% end statement (e.g. @link otter:finish/1.) i.e. it has impact on the 48 | %%% request processing time. Therefore the actions that consume little 49 | %%% time and resources with no external interfaces (e.g. counting in ets) 50 | %%% can be done during the evaluation of the rules, but anything that has 51 | %%% external interface or dependent on environment (e.g. logging and trace 52 | %%% collecting) should be done asynchronously. 53 | %%% @end 54 | %%%------------------------------------------------------------------- 55 | -module(otters_filter). 56 | -include("otters.hrl"). 57 | 58 | -export([span/1]). 59 | 60 | %% The main idea behind this filter that the processing of the spans can 61 | %% be modified runtime by changing the filter configuration. This way 62 | %% logging, counting or sending data to trace collectors can be modified 63 | %% on the running system based on the changing operational requirements. 64 | 65 | %% The span filter works with key value pair lists easiest to implement 66 | %% and reasonably fast. 67 | 68 | %% Filter rules are composed by a list of {Conditions, Actions} tuples. 69 | %% Processing a span means iterating through this list and when an item 70 | %% found where all Conditions evaluate to true then the Actions in that 71 | %% item are executed. 72 | 73 | %% Conditions operate on a copy of the tags of the span. It is a 74 | %% sequence of checks against the tags (e.g. key present, key value) 75 | %% where if any check in the sequence fails, the associated actions are 76 | %% not executed and the next {Conditions, Actions} item is evaluated. 77 | %% Actions can trigger e.g. counting a particular tag value combination, 78 | %% logging, sending the span to trace collectors, modifying the working 79 | %% (i.e. used for further conditions) tag list, modifying the span tag 80 | %% list that is to be sent to trace collector. Actions can also 81 | %% influence the further evaluation of the rules i.e. providing the 82 | %% break action instructs the rule engine NOT to look further in the 83 | %% list of {Conditions, Actions}. 84 | 85 | %% Evaluation of the rules happens in the process which invokes the span 86 | %% end statement (e.g. otters_span:pend/0) i.e. it has impact on the 87 | %% request processing time. Therefore the actions that consume little 88 | %% time and resources with no external interfaces (e.g. counting in ets) 89 | %% can be done during the evaluation of the rules, but anything that has 90 | %% external interface or dependent on environment (e.g. logging and trace 91 | %% collecting) should be done asynchronously. 92 | 93 | -type filter() :: {[term()], [term()]}. 94 | 95 | %%-------------------------------------------------------------------- 96 | %% @doc Takes a span as input, evaluates rules specified in 97 | %% configuration and executes any specified actions. 98 | %% @end 99 | %% -------------------------------------------------------------------- 100 | -spec span(otters:span()) -> ok. 101 | span(#span{tags = Tags, name = Name, duration = Duration} = Span) -> 102 | case otters_config:read(filter_rules, undefined) of 103 | undefined -> 104 | ol:span(Span); 105 | Rules -> 106 | Tags1 = Tags#{ 107 | <<"otters_span_name">> => {Name, undefined}, 108 | <<"otters_span_duration">> => {Duration, undefined} 109 | }, 110 | rules(Rules, Tags1, Span) 111 | end. 112 | 113 | -spec rules([filter()], otter:tags(), otter:span()) -> 114 | ok. 115 | rules([{Conditions, Actions} | Rest], Tags, Span) -> 116 | case check_conditions(Conditions, Tags) of 117 | false -> 118 | rules(Rest, Tags, Span); 119 | true -> 120 | case do_actions(Actions, Tags, Span) of 121 | break -> 122 | ok; 123 | {continue, NewTags, NewSpan} -> 124 | rules(Rest, NewTags, NewSpan) 125 | end 126 | end; 127 | rules(_, _, _) -> 128 | ok. 129 | 130 | check_conditions([Condition | Rest], Tags) -> 131 | case check(Condition, Tags) of 132 | true -> 133 | check_conditions(Rest, Tags); 134 | false -> 135 | false 136 | end; 137 | check_conditions([], _) -> 138 | true. 139 | 140 | 141 | get_tag(Key, Tags) -> 142 | KeyBin = otters_lib:to_bin(Key), 143 | maps:find(KeyBin, Tags). 144 | 145 | check({negate, Condition}, Tags) -> 146 | not check(Condition, Tags); 147 | check({value, Key, Value}, Tags) -> 148 | case get_tag(Key, Tags) of 149 | {ok, {Value, _}} -> 150 | true; 151 | _ -> 152 | false 153 | end; 154 | check({same, Key1, Key2}, Tags) -> 155 | case {get_tag(Key1, Tags), 156 | get_tag(Key2, Tags)} of 157 | {{ok, {Value, _}}, 158 | {ok, {Value, _}}} -> 159 | true; 160 | _ -> 161 | false 162 | end; 163 | check({greater, Key, Value}, Tags) -> 164 | case get_tag(Key, Tags) of 165 | {ok, {Value1, _}} when Value1 > Value -> 166 | true; 167 | _ -> 168 | false 169 | end; 170 | check({less, Key, Value}, Tags) -> 171 | case get_tag(Key, Tags) of 172 | {ok, {Value1, _}} when Value1 < Value -> 173 | true; 174 | _ -> 175 | false 176 | end; 177 | check({between, Key, Value1, Value2}, Tags) -> 178 | case get_tag(Key, Tags) of 179 | {ok, {Value, _}} when Value > Value1 andalso Value < Value2 -> 180 | true; 181 | _ -> 182 | false 183 | end; 184 | check({present, Key}, Tags) -> 185 | case get_tag(Key, Tags) of 186 | {ok, _} -> 187 | true; 188 | _ -> 189 | false 190 | end; 191 | check(_, _) -> 192 | false. 193 | 194 | do_actions(Actions, Tags, Span) -> 195 | do_actions(Actions, Tags, Span, continue). 196 | 197 | do_actions([break | Rest], Tags, Span, _BreakOrContinue) -> 198 | do_actions(Rest, Tags, Span, break); 199 | do_actions([continue | Rest], Tags, Span, _BreakOrContinue) -> 200 | do_actions(Rest, Tags, Span, continue); 201 | do_actions([Action | Rest], Tags, Span, BreakOrContinue) -> 202 | do_actions(Rest, action(Action, Tags, Span), Span, BreakOrContinue); 203 | do_actions([], _Tags, _Span, break) -> 204 | break; 205 | do_actions([], Tags, Span, continue) -> 206 | {continue, Tags, Span}. 207 | 208 | action(send_to_zipkin, Tags, Span) -> 209 | otters_conn_zipkin:store_span(Span), 210 | Tags; 211 | action({snapshot_count, Prefix, TagNames}, Tags, Span) -> 212 | TagValues = [ 213 | case get_tag(Key, Tags) of 214 | {ok, {Value, _}} -> Value; 215 | _ -> undefined 216 | end || Key <- TagNames], 217 | otters_snapshot_count:snapshot(Prefix ++ TagValues, Span), 218 | Tags; 219 | action(_, Tags, _) -> 220 | Tags. 221 | -------------------------------------------------------------------------------- /test/test_httpc.config: -------------------------------------------------------------------------------- 1 | [{otter, [{http_client, httpc}]}]. 2 | -------------------------------------------------------------------------------- /test/test_ibrowse.config: -------------------------------------------------------------------------------- 1 | [{otter, [{http_client, ibrowse}]}]. 2 | --------------------------------------------------------------------------------