├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── bin ├── lint.sh └── tester ├── examples ├── tracing_client.js ├── tracing_server.js ├── zipkin_tracing_client.js └── zipkin_tracing_server.js ├── index.js ├── jshint.json ├── lib ├── _thrift │ ├── zipkinCore.thrift │ └── zipkinCore │ │ └── zipkinCore_types.js ├── formatters.js ├── index.js ├── node_tracers.js ├── trace.js └── tracers.js ├── package.json ├── scripts └── tryfer_format_sample.py └── tests ├── test_formatters.js ├── test_node_tracers.js ├── test_trace.js ├── test_tracers.js └── test_tryfer_compatibility.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.xml 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 5.2 5 | - 4.2 6 | - 0.12 7 | - 0.10 8 | - 0.8 9 | - 0.6 10 | 11 | before_install: 12 | - '[ "${TRAVIS_NODE_VERSION}" != "0.8" ] || npm install -g npm@1.4.28' 13 | - '[ "${TRAVIS_NODE_VERSION}" != "0.6" ] || npm install -g npm@1.3.26' 14 | - '[ "${TRAVIS_NODE_VERSION}" != "0.1*" ] || npm install -g npm@latest' 15 | 16 | before_script: npm run-script lint 17 | script: npm test 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-tryfer: A Node Zipkin Tracer Library 2 | 3 | [![Build Status](https://secure.travis-ci.org/tryfer/node-tryfer.png?branch=master)](http://travis-ci.org/tryfer/node-tryfer) 4 | 5 | Zipkin is a Distributed Tracing system, tryfer is a Python/Twisted client library for Zipkin, and node-tryfer is a port of tryfer. 6 | 7 | Tryfer (and by extention node-tryfer) is heavily influenced by Finagle's tracing libraries. 8 | 9 | --- 10 | 11 | ## HTTP Tracing examples 12 | 13 | To see an example of tracing working in an http client and on an http server, run in one terminal: 14 | 15 | ``` 16 | node examples/tracing_server.js 17 | ``` 18 | 19 | This starts the [express](https://github.com/visionmedia/express) web server, which supports tracing headers from an http client. For example, run the sample client in another terminal, and get the root page from the express web server: 20 | 21 | ``` 22 | node examples/tracing_client.js GET http://localhost:8080/ 23 | ``` 24 | 25 | In the client terminal, you will see a list of all the traces that the client records, which look like this: 26 | 27 | --- Trace --- 28 | [ 29 | { 30 | "trace_id": "55bad31800000000", 31 | "span_id": "6ec7e90a00000000", 32 | "name": "GET", 33 | "annotations": [ 34 | { 35 | "key": "http.uri", 36 | "value": "http://localhost:8080/", 37 | "type": "string" 38 | } 39 | ] 40 | } 41 | ] 42 | 43 | There should be a URI annotation, a client send annotation, a client receive annotation, and extra string annotations documenting aspects of the response from the server. 44 | 45 | In the server terminal, you will see a list of all the traces that the server records, which look like this: 46 | 47 | --- Trace --- 48 | [ 49 | { 50 | "trace_id": "55bad31800000000", 51 | "span_id": "6ec7e90a00000000", 52 | "name": "GET", 53 | "annotations": [ 54 | { 55 | "key": "sr", 56 | "value": 1346194548062000, 57 | "type": "timestamp", 58 | "host": { 59 | "ipv4": "127.0.0.1", 60 | "port": 8080, 61 | "service_name": "example-http-server" 62 | } 63 | } 64 | ] 65 | } 66 | ] 67 | 68 | Note that the trace id and the span id are the same, showing that this is all part of the same trace. In addition, the server adds its endpoint to the trace (where the server is running). 69 | 70 | There should be a server receive annotation, zero or more string annotations containing information about the request itself, and then a server send annotation. 71 | 72 | In both the client and the server example, the debug tracer is used to print out the traces. Not all tracers do something as soon as `record` is called - some tracers may batch up annotations and/or traces, as opposed to shipping them to zipkin as soon as `record` as called. 73 | -------------------------------------------------------------------------------- /bin/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2012 Rackspace 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | ./node_modules/.bin/jshint lib/*.js tests/*.js examples/*.js --config jshint.json 18 | -------------------------------------------------------------------------------- /bin/tester: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2012 Rackspace 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | basedir=`dirname $0` 18 | 19 | ./node_modules/.bin/nodeunit tests 20 | 21 | echo "Running linter..." 22 | -------------------------------------------------------------------------------- /examples/tracing_client.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Rackspace Hosting, 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 | 15 | 16 | // USAGE: node tracing_client.js [HTTP Method] [URL] 17 | // Sample client code showing how to make an HTTP request to a server that 18 | // supports tracing (one that uses node-tryfer, tryfer, or finagle) 19 | 20 | var request = require('request'); 21 | 22 | var trace = require('..').trace; 23 | var tracers = require('..').tracers; 24 | 25 | var method, uri, t; 26 | 27 | if (process.argv.length < 4) { 28 | console.log("Usage: " + 29 | process.argv.slice(0, 2).concat(['method', 'uri']).join(" ")); 30 | process.exit(1); 31 | } 32 | 33 | method = process.argv[2]; 34 | uri = process.argv[3]; 35 | 36 | // DebugTracer prints traces to stdout 37 | tracers.pushTracer(new tracers.DebugTracer(process.stdout)); 38 | 39 | // The trace used for http requests, by Tryfer convention, should be named by 40 | // the request method 41 | t = new trace.Trace('GET'); 42 | 43 | // record the URI and send out a CLIENT_SEND annotation before actually making 44 | // the request 45 | t.record(trace.Annotation.uri(uri)); 46 | t.record(trace.Annotation.clientSend()); 47 | 48 | // Make the request after the CLIENT_SEND annotation has been recorded 49 | request( 50 | {method: method, uri: uri, headers: t.toHeaders()}, 51 | function (error, response, body) { 52 | 53 | // by Tryfer convention, and by what finagle does, a CLIENT_RECV annotation 54 | // should be made as soon as a response is returned 55 | t.record(trace.Annotation.clientRecv()); 56 | 57 | // Other annotations based on the response can also be made 58 | if (error !== undefined && error !== null) { 59 | t.record(trace.Annotation.string(error.toString())); 60 | } else { 61 | t.record(trace.Annotation.string('http.response.code', 62 | response.statusCode.toString())); 63 | t.record(trace.Annotation.string('http.request.headers', JSON.stringify(response.headers))); 64 | t.record(trace.Annotation.string('http.response.body', body)); 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /examples/tracing_server.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Rackspace Hosting, 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 | 15 | 16 | // USAGE: node tracing_server.js 17 | // Sample server code showing how to make a server that supports tracing 18 | // HTTP requests from clients 19 | 20 | var express = require('express'); 21 | 22 | var trace = require('..').trace; 23 | var tracers = require('..').tracers; 24 | 25 | // DebugTracer prints traces to stdout 26 | tracers.pushTracer(new tracers.DebugTracer(process.stdout)); 27 | 28 | var app = express(); 29 | 30 | app.get('/', function(request, response) { 31 | // Create a trace from the request. Alternately, 32 | // {trace.Trace.fromHeaders} could also be used (called with the request 33 | // method name and the request headers, because by Tryfer convention, the 34 | // trace used for http requests should be named by the request method) 35 | // but {trace.Trace.fromRequest} also adds an endpoint based on the socket 36 | // on the request. 37 | var t = trace.Trace.fromRequest(request, 'example-http-server'); 38 | // Record the server receive annotation as soon as possible 39 | t.record(trace.Annotation.serverRecv()); 40 | 41 | // Other annotations can also be recorded 42 | t.record(trace.Annotation.string('request_headers', 43 | JSON.stringify(request.headers))); 44 | response.statusCode = 200; 45 | response.write('this is the body'); 46 | response.end(); 47 | 48 | // By Tryfer convention, and by what finagle does, a SERVER_SEND annotation 49 | // should be made as soon as a response is sent 50 | t.record(trace.Annotation.serverSend()); 51 | }); 52 | 53 | app.listen(8080, 'localhost'); 54 | -------------------------------------------------------------------------------- /examples/zipkin_tracing_client.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Rackspace Hosting, 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 | 15 | 16 | // USAGE: node zipkin_tracing_client.js [HTTP Method] [URL] 17 | // Sample client code showing how to make an HTTP request to a server that 18 | // supports tracing (one that uses node-tryfer, tryfer, or finagle) 19 | 20 | var request = require('request'); 21 | 22 | var trace = require('..').trace; 23 | var tracers = require('..').tracers; 24 | var nodeTracers = require('..').node_tracers; 25 | 26 | var Scribe = require('scribe').Scribe; 27 | 28 | var method, uri, t; 29 | 30 | if (process.argv.length < 4) { 31 | console.log("Usage: " + 32 | process.argv.slice(0, 2).concat(['method', 'uri']).join(" ")); 33 | process.exit(1); 34 | } 35 | 36 | method = process.argv[2]; 37 | uri = process.argv[3]; 38 | 39 | // Zipkin sends traces to Scribe 40 | var scribeClient = new Scribe('localhost',1463,{autoReconnect:true}); 41 | scribeClient.open(function(err){ 42 | console.log(err); 43 | }); 44 | tracers.pushTracer(new nodeTracers.ZipkinTracer(scribeClient)); 45 | // DebugTracer prints traces to stdout 46 | tracers.pushTracer(new tracers.DebugTracer(process.stdout)); 47 | 48 | // The trace used for http requests, by Tryfer convention, should be named by 49 | // the request method 50 | t = new trace.Trace('GET'); 51 | 52 | // record the URI and send out a CLIENT_SEND annotation before actually making 53 | // the request 54 | t.record(trace.Annotation.uri(uri)); 55 | t.record(trace.Annotation.clientSend()); 56 | 57 | // Make the request after the CLIENT_SEND annotation has been recorded 58 | request( 59 | {method: method, uri: uri, headers: t.toHeaders()}, 60 | function (error, response, body) { 61 | 62 | // By Tryfer convention, and by what finagle does, a CLIENT_RECV 63 | // annotation should be made as soon as a response is returned. 64 | // However, including additional annotations based on the response 65 | // would be useful, and these have to be recorded before the 66 | // CLIENT_RECV is sent, since that the CLIENT_RECV ends the trace. 67 | 68 | // Response-based annotations 69 | if (error !== undefined && error !== null) { 70 | t.record(trace.Annotation.string(error.toString())); 71 | } else { 72 | t.record(trace.Annotation.string('http.response.code', 73 | response.statusCode.toString())); 74 | t.record(trace.Annotation.string('http.request.headers', JSON.stringify(response.headers))); 75 | t.record(trace.Annotation.string('http.response.body', body)); 76 | } 77 | 78 | // CLIENT_RECV 79 | t.record(trace.Annotation.clientRecv()); 80 | }); 81 | -------------------------------------------------------------------------------- /examples/zipkin_tracing_server.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Rackspace Hosting, 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 | 15 | 16 | // USAGE: node zipkin_tracing_server.js 17 | // Sample server code showing how to make a server that supports tracing with zipkin 18 | // HTTP requests from clients 19 | 20 | var express = require('express'); 21 | 22 | var trace = require('..').trace; 23 | var tracers = require('..').tracers; 24 | var nodeTracers = require('..').node_tracers; 25 | 26 | var Scribe = require('scribe').Scribe; 27 | 28 | // ZipkinTracer - requires Scribe running locally. 29 | var scribeClient = new Scribe('localhost',1463,{autoReconnect:true}); 30 | scribeClient.open(function(err){ 31 | console.log(err); 32 | }); 33 | var zipkinTracer = new nodeTracers.ZipkinTracer(scribeClient,'test',{maxTraces:5}); 34 | tracers.pushTracer(zipkinTracer); 35 | // DebugTracer prints traces to stdout 36 | tracers.pushTracer(new tracers.DebugTracer(process.stdout)); 37 | 38 | var app = express(); 39 | 40 | app.get('/', function(request, response) { 41 | // Create a trace from the request. Alternately, 42 | // {trace.Trace.fromHeaders} could also be used (called with the request 43 | // method name and the request headers, because by Tryfer convention, the 44 | // trace used for http requests should be named by the request method) 45 | // but {trace.Trace.fromRequest} also adds an endpoint based on the socket 46 | // on the request. 47 | var t = trace.Trace.fromRequest(request, 'example-http-server'); 48 | // Record the server receive annotation as soon as possible 49 | t.record(trace.Annotation.serverRecv()); 50 | 51 | // Other annotations can also be recorded 52 | t.record(trace.Annotation.string('request_headers', 53 | JSON.stringify(request.headers))); 54 | response.statusCode = 200; 55 | response.write('this is the body'); 56 | response.end(); 57 | 58 | // By Tryfer convention, and by what finagle does, a SERVER_SEND annotation 59 | // should be made as soon as a response is sent 60 | t.record(trace.Annotation.serverSend()); 61 | }); 62 | 63 | app.listen(8080, 'localhost'); 64 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Rackspace Hosting, 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 | 15 | module.exports = require('./lib'); 16 | -------------------------------------------------------------------------------- /jshint.json: -------------------------------------------------------------------------------- 1 | { 2 | "maxerr" : 100, 3 | "node" : true, 4 | "curly" : true, 5 | "eqeqeq" : true, 6 | "latedef" : false, 7 | "undef" : true, 8 | "newcap" : true, 9 | "nonew" : true, 10 | "onevar" : false, 11 | "trailing" : true, 12 | "white" : false, 13 | "sub" : true, 14 | "evil" : true, 15 | "onecase" : true, 16 | "strict" : false 17 | } 18 | -------------------------------------------------------------------------------- /lib/_thrift/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 | #************** Collection related structs ************** 19 | 20 | # these are the annotations we always expect to find in a span 21 | const string CLIENT_SEND = "cs" 22 | const string CLIENT_RECV = "cr" 23 | const string SERVER_SEND = "ss" 24 | const string SERVER_RECV = "sr" 25 | 26 | # this represents a host and port in a network 27 | struct Endpoint { 28 | 1: i32 ipv4, 29 | 2: i16 port # beware that this will give us negative ports. some conversion needed 30 | 3: string service_name # which service did this operation happen on? 31 | } 32 | 33 | # some event took place, either one by the framework or by the user 34 | struct Annotation { 35 | 1: i64 timestamp # microseconds from epoch 36 | 2: string value # what happened at the timestamp? 37 | 3: optional Endpoint host # host this happened on 38 | 4: optional i32 duration # how long did the operation take? microseconds 39 | } 40 | 41 | enum AnnotationType { BOOL, BYTES, I16, I32, I64, DOUBLE, STRING } 42 | 43 | struct BinaryAnnotation { 44 | 1: string key, 45 | 2: binary value, 46 | 3: AnnotationType annotation_type, 47 | 4: optional Endpoint host 48 | } 49 | 50 | struct Span { 51 | 1: i64 trace_id # unique trace id, use for all spans in trace 52 | 3: string name, # span name, rpc method for example 53 | 4: i64 id, # unique span id, only used for this span 54 | 5: optional i64 parent_id, # parent span id 55 | 6: list annotations, # list of all annotations/events that occured 56 | 8: list binary_annotations # any binary annotations 57 | 9: optional bool debug = 0 # if true, we DEMAND that this span passes all samplers 58 | } 59 | 60 | -------------------------------------------------------------------------------- /lib/_thrift/zipkinCore/zipkinCore_types.js: -------------------------------------------------------------------------------- 1 | // 2 | // Autogenerated by Thrift Compiler (0.9.2) 3 | // 4 | // DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING 5 | // 6 | var thrift = require('thrift'); 7 | var Thrift = thrift.Thrift; 8 | var Q = thrift.Q; 9 | 10 | 11 | var ttypes = module.exports = {}; 12 | ttypes.AnnotationType = { 13 | 'BOOL' : 0, 14 | 'BYTES' : 1, 15 | 'I16' : 2, 16 | 'I32' : 3, 17 | 'I64' : 4, 18 | 'DOUBLE' : 5, 19 | 'STRING' : 6 20 | }; 21 | var Endpoint = module.exports.Endpoint = function(args) { 22 | this.ipv4 = null; 23 | this.port = null; 24 | this.service_name = null; 25 | if (args) { 26 | if (args.ipv4 !== undefined) { 27 | this.ipv4 = args.ipv4; 28 | } 29 | if (args.port !== undefined) { 30 | this.port = args.port; 31 | } 32 | if (args.service_name !== undefined) { 33 | this.service_name = args.service_name; 34 | } 35 | } 36 | }; 37 | Endpoint.prototype = {}; 38 | Endpoint.prototype.read = function(input) { 39 | input.readStructBegin(); 40 | while (true) 41 | { 42 | var ret = input.readFieldBegin(); 43 | var fname = ret.fname; 44 | var ftype = ret.ftype; 45 | var fid = ret.fid; 46 | if (ftype == Thrift.Type.STOP) { 47 | break; 48 | } 49 | switch (fid) 50 | { 51 | case 1: 52 | if (ftype == Thrift.Type.I32) { 53 | this.ipv4 = input.readI32(); 54 | } else { 55 | input.skip(ftype); 56 | } 57 | break; 58 | case 2: 59 | if (ftype == Thrift.Type.I16) { 60 | this.port = input.readI16(); 61 | } else { 62 | input.skip(ftype); 63 | } 64 | break; 65 | case 3: 66 | if (ftype == Thrift.Type.STRING) { 67 | this.service_name = input.readString(); 68 | } else { 69 | input.skip(ftype); 70 | } 71 | break; 72 | default: 73 | input.skip(ftype); 74 | } 75 | input.readFieldEnd(); 76 | } 77 | input.readStructEnd(); 78 | return; 79 | }; 80 | 81 | Endpoint.prototype.write = function(output) { 82 | output.writeStructBegin('Endpoint'); 83 | if (this.ipv4 !== null && this.ipv4 !== undefined) { 84 | output.writeFieldBegin('ipv4', Thrift.Type.I32, 1); 85 | output.writeI32(this.ipv4); 86 | output.writeFieldEnd(); 87 | } 88 | if (this.port !== null && this.port !== undefined) { 89 | output.writeFieldBegin('port', Thrift.Type.I16, 2); 90 | output.writeI16(this.port); 91 | output.writeFieldEnd(); 92 | } 93 | if (this.service_name !== null && this.service_name !== undefined) { 94 | output.writeFieldBegin('service_name', Thrift.Type.STRING, 3); 95 | output.writeString(this.service_name); 96 | output.writeFieldEnd(); 97 | } 98 | output.writeFieldStop(); 99 | output.writeStructEnd(); 100 | return; 101 | }; 102 | 103 | var Annotation = module.exports.Annotation = function(args) { 104 | this.timestamp = null; 105 | this.value = null; 106 | this.host = null; 107 | this.duration = null; 108 | if (args) { 109 | if (args.timestamp !== undefined) { 110 | this.timestamp = args.timestamp; 111 | } 112 | if (args.value !== undefined) { 113 | this.value = args.value; 114 | } 115 | if (args.host !== undefined) { 116 | this.host = args.host; 117 | } 118 | if (args.duration !== undefined) { 119 | this.duration = args.duration; 120 | } 121 | } 122 | }; 123 | Annotation.prototype = {}; 124 | Annotation.prototype.read = function(input) { 125 | input.readStructBegin(); 126 | while (true) 127 | { 128 | var ret = input.readFieldBegin(); 129 | var fname = ret.fname; 130 | var ftype = ret.ftype; 131 | var fid = ret.fid; 132 | if (ftype == Thrift.Type.STOP) { 133 | break; 134 | } 135 | switch (fid) 136 | { 137 | case 1: 138 | if (ftype == Thrift.Type.I64) { 139 | this.timestamp = input.readI64(); 140 | } else { 141 | input.skip(ftype); 142 | } 143 | break; 144 | case 2: 145 | if (ftype == Thrift.Type.STRING) { 146 | this.value = input.readString(); 147 | } else { 148 | input.skip(ftype); 149 | } 150 | break; 151 | case 3: 152 | if (ftype == Thrift.Type.STRUCT) { 153 | this.host = new ttypes.Endpoint(); 154 | this.host.read(input); 155 | } else { 156 | input.skip(ftype); 157 | } 158 | break; 159 | case 4: 160 | if (ftype == Thrift.Type.I32) { 161 | this.duration = input.readI32(); 162 | } else { 163 | input.skip(ftype); 164 | } 165 | break; 166 | default: 167 | input.skip(ftype); 168 | } 169 | input.readFieldEnd(); 170 | } 171 | input.readStructEnd(); 172 | return; 173 | }; 174 | 175 | Annotation.prototype.write = function(output) { 176 | output.writeStructBegin('Annotation'); 177 | if (this.timestamp !== null && this.timestamp !== undefined) { 178 | output.writeFieldBegin('timestamp', Thrift.Type.I64, 1); 179 | output.writeI64(this.timestamp); 180 | output.writeFieldEnd(); 181 | } 182 | if (this.value !== null && this.value !== undefined) { 183 | output.writeFieldBegin('value', Thrift.Type.STRING, 2); 184 | output.writeString(this.value); 185 | output.writeFieldEnd(); 186 | } 187 | if (this.host !== null && this.host !== undefined) { 188 | output.writeFieldBegin('host', Thrift.Type.STRUCT, 3); 189 | this.host.write(output); 190 | output.writeFieldEnd(); 191 | } 192 | if (this.duration !== null && this.duration !== undefined) { 193 | output.writeFieldBegin('duration', Thrift.Type.I32, 4); 194 | output.writeI32(this.duration); 195 | output.writeFieldEnd(); 196 | } 197 | output.writeFieldStop(); 198 | output.writeStructEnd(); 199 | return; 200 | }; 201 | 202 | var BinaryAnnotation = module.exports.BinaryAnnotation = function(args) { 203 | this.key = null; 204 | this.value = null; 205 | this.annotation_type = null; 206 | this.host = null; 207 | if (args) { 208 | if (args.key !== undefined) { 209 | this.key = args.key; 210 | } 211 | if (args.value !== undefined) { 212 | this.value = args.value; 213 | } 214 | if (args.annotation_type !== undefined) { 215 | this.annotation_type = args.annotation_type; 216 | } 217 | if (args.host !== undefined) { 218 | this.host = args.host; 219 | } 220 | } 221 | }; 222 | BinaryAnnotation.prototype = {}; 223 | BinaryAnnotation.prototype.read = function(input) { 224 | input.readStructBegin(); 225 | while (true) 226 | { 227 | var ret = input.readFieldBegin(); 228 | var fname = ret.fname; 229 | var ftype = ret.ftype; 230 | var fid = ret.fid; 231 | if (ftype == Thrift.Type.STOP) { 232 | break; 233 | } 234 | switch (fid) 235 | { 236 | case 1: 237 | if (ftype == Thrift.Type.STRING) { 238 | this.key = input.readString(); 239 | } else { 240 | input.skip(ftype); 241 | } 242 | break; 243 | case 2: 244 | if (ftype == Thrift.Type.STRING) { 245 | this.value = input.readBinary(); 246 | } else { 247 | input.skip(ftype); 248 | } 249 | break; 250 | case 3: 251 | if (ftype == Thrift.Type.I32) { 252 | this.annotation_type = input.readI32(); 253 | } else { 254 | input.skip(ftype); 255 | } 256 | break; 257 | case 4: 258 | if (ftype == Thrift.Type.STRUCT) { 259 | this.host = new ttypes.Endpoint(); 260 | this.host.read(input); 261 | } else { 262 | input.skip(ftype); 263 | } 264 | break; 265 | default: 266 | input.skip(ftype); 267 | } 268 | input.readFieldEnd(); 269 | } 270 | input.readStructEnd(); 271 | return; 272 | }; 273 | 274 | BinaryAnnotation.prototype.write = function(output) { 275 | output.writeStructBegin('BinaryAnnotation'); 276 | if (this.key !== null && this.key !== undefined) { 277 | output.writeFieldBegin('key', Thrift.Type.STRING, 1); 278 | output.writeString(this.key); 279 | output.writeFieldEnd(); 280 | } 281 | if (this.value !== null && this.value !== undefined) { 282 | output.writeFieldBegin('value', Thrift.Type.STRING, 2); 283 | output.writeBinary(this.value); 284 | output.writeFieldEnd(); 285 | } 286 | if (this.annotation_type !== null && this.annotation_type !== undefined) { 287 | output.writeFieldBegin('annotation_type', Thrift.Type.I32, 3); 288 | output.writeI32(this.annotation_type); 289 | output.writeFieldEnd(); 290 | } 291 | if (this.host !== null && this.host !== undefined) { 292 | output.writeFieldBegin('host', Thrift.Type.STRUCT, 4); 293 | this.host.write(output); 294 | output.writeFieldEnd(); 295 | } 296 | output.writeFieldStop(); 297 | output.writeStructEnd(); 298 | return; 299 | }; 300 | 301 | var Span = module.exports.Span = function(args) { 302 | this.trace_id = null; 303 | this.name = null; 304 | this.id = null; 305 | this.parent_id = null; 306 | this.annotations = null; 307 | this.binary_annotations = null; 308 | this.debug = false; 309 | if (args) { 310 | if (args.trace_id !== undefined) { 311 | this.trace_id = args.trace_id; 312 | } 313 | if (args.name !== undefined) { 314 | this.name = args.name; 315 | } 316 | if (args.id !== undefined) { 317 | this.id = args.id; 318 | } 319 | if (args.parent_id !== undefined) { 320 | this.parent_id = args.parent_id; 321 | } 322 | if (args.annotations !== undefined) { 323 | this.annotations = args.annotations; 324 | } 325 | if (args.binary_annotations !== undefined) { 326 | this.binary_annotations = args.binary_annotations; 327 | } 328 | if (args.debug !== undefined) { 329 | this.debug = args.debug; 330 | } 331 | } 332 | }; 333 | Span.prototype = {}; 334 | Span.prototype.read = function(input) { 335 | input.readStructBegin(); 336 | while (true) 337 | { 338 | var ret = input.readFieldBegin(); 339 | var fname = ret.fname; 340 | var ftype = ret.ftype; 341 | var fid = ret.fid; 342 | if (ftype == Thrift.Type.STOP) { 343 | break; 344 | } 345 | switch (fid) 346 | { 347 | case 1: 348 | if (ftype == Thrift.Type.I64) { 349 | this.trace_id = input.readI64(); 350 | } else { 351 | input.skip(ftype); 352 | } 353 | break; 354 | case 3: 355 | if (ftype == Thrift.Type.STRING) { 356 | this.name = input.readString(); 357 | } else { 358 | input.skip(ftype); 359 | } 360 | break; 361 | case 4: 362 | if (ftype == Thrift.Type.I64) { 363 | this.id = input.readI64(); 364 | } else { 365 | input.skip(ftype); 366 | } 367 | break; 368 | case 5: 369 | if (ftype == Thrift.Type.I64) { 370 | this.parent_id = input.readI64(); 371 | } else { 372 | input.skip(ftype); 373 | } 374 | break; 375 | case 6: 376 | if (ftype == Thrift.Type.LIST) { 377 | var _size0 = 0; 378 | var _rtmp34; 379 | this.annotations = []; 380 | var _etype3 = 0; 381 | _rtmp34 = input.readListBegin(); 382 | _etype3 = _rtmp34.etype; 383 | _size0 = _rtmp34.size; 384 | for (var _i5 = 0; _i5 < _size0; ++_i5) 385 | { 386 | var elem6 = null; 387 | elem6 = new ttypes.Annotation(); 388 | elem6.read(input); 389 | this.annotations.push(elem6); 390 | } 391 | input.readListEnd(); 392 | } else { 393 | input.skip(ftype); 394 | } 395 | break; 396 | case 8: 397 | if (ftype == Thrift.Type.LIST) { 398 | var _size7 = 0; 399 | var _rtmp311; 400 | this.binary_annotations = []; 401 | var _etype10 = 0; 402 | _rtmp311 = input.readListBegin(); 403 | _etype10 = _rtmp311.etype; 404 | _size7 = _rtmp311.size; 405 | for (var _i12 = 0; _i12 < _size7; ++_i12) 406 | { 407 | var elem13 = null; 408 | elem13 = new ttypes.BinaryAnnotation(); 409 | elem13.read(input); 410 | this.binary_annotations.push(elem13); 411 | } 412 | input.readListEnd(); 413 | } else { 414 | input.skip(ftype); 415 | } 416 | break; 417 | case 9: 418 | if (ftype == Thrift.Type.BOOL) { 419 | this.debug = input.readBool(); 420 | } else { 421 | input.skip(ftype); 422 | } 423 | break; 424 | default: 425 | input.skip(ftype); 426 | } 427 | input.readFieldEnd(); 428 | } 429 | input.readStructEnd(); 430 | return; 431 | }; 432 | 433 | Span.prototype.write = function(output) { 434 | output.writeStructBegin('Span'); 435 | if (this.trace_id !== null && this.trace_id !== undefined) { 436 | output.writeFieldBegin('trace_id', Thrift.Type.I64, 1); 437 | output.writeI64(this.trace_id); 438 | output.writeFieldEnd(); 439 | } 440 | if (this.name !== null && this.name !== undefined) { 441 | output.writeFieldBegin('name', Thrift.Type.STRING, 3); 442 | output.writeString(this.name); 443 | output.writeFieldEnd(); 444 | } 445 | if (this.id !== null && this.id !== undefined) { 446 | output.writeFieldBegin('id', Thrift.Type.I64, 4); 447 | output.writeI64(this.id); 448 | output.writeFieldEnd(); 449 | } 450 | if (this.parent_id !== null && this.parent_id !== undefined) { 451 | output.writeFieldBegin('parent_id', Thrift.Type.I64, 5); 452 | output.writeI64(this.parent_id); 453 | output.writeFieldEnd(); 454 | } 455 | if (this.annotations !== null && this.annotations !== undefined) { 456 | output.writeFieldBegin('annotations', Thrift.Type.LIST, 6); 457 | output.writeListBegin(Thrift.Type.STRUCT, this.annotations.length); 458 | for (var iter14 in this.annotations) 459 | { 460 | if (this.annotations.hasOwnProperty(iter14)) 461 | { 462 | iter14 = this.annotations[iter14]; 463 | iter14.write(output); 464 | } 465 | } 466 | output.writeListEnd(); 467 | output.writeFieldEnd(); 468 | } 469 | if (this.binary_annotations !== null && this.binary_annotations !== undefined) { 470 | output.writeFieldBegin('binary_annotations', Thrift.Type.LIST, 8); 471 | output.writeListBegin(Thrift.Type.STRUCT, this.binary_annotations.length); 472 | for (var iter15 in this.binary_annotations) 473 | { 474 | if (this.binary_annotations.hasOwnProperty(iter15)) 475 | { 476 | iter15 = this.binary_annotations[iter15]; 477 | iter15.write(output); 478 | } 479 | } 480 | output.writeListEnd(); 481 | output.writeFieldEnd(); 482 | } 483 | if (this.debug !== null && this.debug !== undefined) { 484 | output.writeFieldBegin('debug', Thrift.Type.BOOL, 9); 485 | output.writeBool(this.debug); 486 | output.writeFieldEnd(); 487 | } 488 | output.writeFieldStop(); 489 | output.writeStructEnd(); 490 | return; 491 | }; 492 | 493 | ttypes.CLIENT_SEND = 'cs'; 494 | ttypes.CLIENT_RECV = 'cr'; 495 | ttypes.SERVER_SEND = 'ss'; 496 | ttypes.SERVER_RECV = 'sr'; 497 | -------------------------------------------------------------------------------- /lib/formatters.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Rackspace Hosting, 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 | 15 | /** 16 | * Formatters that can be used by tracers. RESTkin expects the trace and 17 | * annotations to be a particular JSON format, and Zipkin/Scribe expects 18 | * base64-encoded thrift objects. 19 | */ 20 | 21 | (function () { 22 | var async = require('async'); 23 | 24 | var formatForRestkin, formatForZipkin, formatForQueryService, hexStringify, ipv4ToNumber; 25 | var zipkinCore_types, thrift, ttransport, tprotocol, Int64; 26 | var _formatForRestkin, _formatForQueryService; 27 | 28 | /** 29 | * Hexifies a string and zero-pads it until it is at least the desired length 30 | * 31 | * @param {Number} number The number to turn into a zero-padded hex string 32 | */ 33 | hexStringify = function (number) { 34 | var num = new Int64(number); 35 | return num.toOctetString(); 36 | }; 37 | 38 | /** 39 | * Formats the trace and annotation to be accepted by RESTkin - note that 40 | * the trace_id, span_id, and parent_span_id are zero-padded hex strings of 41 | * length 16 42 | * 43 | * @param {Trace} trace Trace to format 44 | * @param {Annotation[]} annotations Annotations to format 45 | * @param {Function} callback Callback called with (err, formattedResult). 46 | */ 47 | _formatForRestkin = function (trace, annotations, callback) { 48 | var self = this; 49 | var annJson; 50 | var restkinTrace = { 51 | trace_id: trace.traceId, 52 | span_id: trace.spanId, 53 | name: trace.name, 54 | annotations: [] 55 | }; 56 | 57 | if (trace.parentSpanId !== undefined) { 58 | restkinTrace.parent_span_id = trace.parentSpanId; 59 | } 60 | 61 | if (trace.debug !== undefined) { 62 | restkinTrace.debug = trace.debug; 63 | } 64 | 65 | Array.prototype.forEach.call(annotations, function(ann, idx) { 66 | annJson = { 67 | key: ann.name, 68 | value: ann.value, 69 | type: ann.annotationType 70 | }; 71 | 72 | if (ann.endpoint !== undefined) { 73 | annJson.host = { 74 | ipv4: ann.endpoint.ipv4, 75 | port: ann.endpoint.port, 76 | service_name: ann.endpoint.serviceName 77 | }; 78 | } 79 | 80 | if (ann.duration !== undefined) { 81 | annJson.duration = ann.duration; 82 | } 83 | 84 | restkinTrace.annotations.push(annJson); 85 | }); 86 | 87 | callback(null, restkinTrace); 88 | }; 89 | 90 | /** 91 | * Format multiple traces and annotations for RESTkin. 92 | * 93 | * @param {Array} values An array of [trace, annotations] tuples. 94 | * @param {Function} callback Callback called with (err, formattedResult). 95 | */ 96 | formatForRestkin = function (values, callback) { 97 | async.map(values, function(tuple, callback) { 98 | var trace, annotations; 99 | 100 | trace = tuple[0]; 101 | annotations = tuple[1]; 102 | _formatForRestkin(trace, annotations, callback); 103 | }, 104 | 105 | function(err, results) { 106 | if (err) { 107 | callback(err); 108 | return; 109 | } 110 | 111 | callback(null, JSON.stringify(results, null, 2)); 112 | }); 113 | }; 114 | 115 | /** 116 | * Formats the trace and annotation to be accepted by Zipkin Query Collector Service API 117 | * 118 | * @param {Trace} trace Trace to format 119 | * @param {Annotation[]} annotations Annotations to format 120 | * @param {Function} callback Callback called with (err, formattedResult). 121 | */ 122 | _formatForQueryService = function (trace, annotations, callback) { 123 | var self = this; 124 | var annJson; 125 | var zipkinTrace = { 126 | traceId: trace.traceId, 127 | id: trace.spanId, 128 | name: trace.name, 129 | annotations: [], 130 | binaryAnnotations: [] 131 | }; 132 | 133 | if (trace.parentSpanId !== undefined) { 134 | zipkinTrace.parentId = trace.parentSpanId; 135 | } 136 | 137 | Array.prototype.forEach.call(annotations, function(ann, idx) { 138 | annJson = {}; 139 | if (ann.endpoint !== undefined) { 140 | annJson.endpoint = { 141 | ipv4: ann.endpoint.ipv4, 142 | port: ann.endpoint.port, 143 | serviceName: ann.endpoint.serviceName 144 | }; 145 | } 146 | 147 | if (ann.annotationType === 'timestamp') { 148 | annJson.timestamp = ann.value; 149 | annJson.value = ann.name; 150 | zipkinTrace.annotations.push(annJson); 151 | } else { 152 | annJson.key = ann.name; 153 | annJson.value = ann.value.toString(); 154 | annJson.type = ann.annotationType; 155 | zipkinTrace.binaryAnnotations.push(annJson); 156 | } 157 | }); 158 | 159 | callback(null, zipkinTrace); 160 | }; 161 | 162 | /** 163 | * Formats multiple traces and annotations for Zipkin Query Collector Service 164 | * @param {[type]} trace [description] 165 | * @return {[type]} [description] 166 | */ 167 | formatForQueryService = function (values, callback) { 168 | async.map(values, function(tuple, callback) { 169 | var trace, annotations; 170 | 171 | trace = tuple[0]; 172 | annotations = tuple[1]; 173 | _formatForQueryService(trace, annotations, callback); 174 | }, 175 | 176 | function(err, results) { 177 | if (err) { 178 | callback(err); 179 | return; 180 | } 181 | 182 | callback(null, JSON.stringify(results, null, 2)); 183 | }); 184 | }; 185 | 186 | /*** ----- Node-specific, since the thrift relies on Buffers ---- ***/ 187 | 188 | /** 189 | * Converts an IPv4 address ({[0-255].[0-255].[0-255].[0-255]}) to a number. 190 | * If the address is ill-formatted, raises an error 191 | * 192 | * formula is (first octet * 256^3) + (second octet * 256^2) + 193 | * (third octet * 256) + (fourth octet) 194 | * 195 | * @param {String} ipv4 String containing the IPv4 address 196 | */ 197 | ipv4ToNumber = function(ipv4) { 198 | var octets = ipv4.split("."); 199 | var sum = 0, i; 200 | 201 | if(octets.length !== 4) { 202 | throw new Error("IPv4 string does not have 4 parts."); 203 | } 204 | 205 | for (i=0; i<4; i++) { 206 | var octet = parseInt(octets[i], 10); 207 | if (isNaN(octet) || octet < 0 || octet > 255) { 208 | throw new Error("IPv4 string contains a value that is not a number " + 209 | "between 0 and 255 inclusive"); 210 | } 211 | 212 | sum = (sum << 8) + octet; 213 | } 214 | 215 | return sum; 216 | }; 217 | 218 | /** 219 | * Formats the trace and annotation to be accepted by Zipkin over Scribe, 220 | * which takes base 64 thrift 221 | * 222 | * @param {Trace} trace Trace to format 223 | * @param {Annotation[]} annotations Annotations to format 224 | * @param {Function} callback Callback to call with the restkin-formatted 225 | * trace 226 | */ 227 | formatForZipkin = function(trace, annotations, callback) { 228 | var self = this; 229 | var thriftAnn = [], binaryAnn = [], 230 | ttype_args, trans, prot, tspan; 231 | 232 | Array.prototype.forEach.call(annotations, function(ann, idx) { 233 | ttype_args = {value: ann.value}; 234 | if (ann.endpoint !== undefined) { 235 | ttype_args.host = new zipkinCore_types.Endpoint({ 236 | ipv4: ipv4ToNumber(ann.endpoint.ipv4), 237 | port: ann.endpoint.port, 238 | service_name: ann.endpoint.serviceName 239 | }); 240 | } 241 | 242 | if (ann.annotationType === 'timestamp') { 243 | ttype_args.timestamp = ann.value; 244 | ttype_args.value = ann.name; 245 | ttype_args.duration = ann.duration; 246 | thriftAnn.push(new zipkinCore_types.Annotation(ttype_args)); 247 | } else { 248 | ttype_args.key = ann.name; 249 | ttype_args.value = ttype_args.value.toString(); 250 | ttype_args.annotation_type = zipkinCore_types.AnnotationType.STRING; 251 | binaryAnn.push(new zipkinCore_types.BinaryAnnotation(ttype_args)); 252 | } 253 | }); 254 | 255 | tspan = new zipkinCore_types.Span({ 256 | trace_id: trace.traceId, 257 | name: trace.name, 258 | id: trace.spanId, 259 | parent_id: trace.parentSpanId, 260 | debug: trace.debug, 261 | annotations: thriftAnn, 262 | binary_annotations: binaryAnn 263 | }); 264 | 265 | // HERE do something to base64 encode thrift stuff 266 | trans = new ttransport.TBufferedTransport(null, function(buffer) { 267 | if (callback !== undefined) { 268 | callback(null, buffer.toString('base64').trim()); 269 | } 270 | }); 271 | // writeBufferSize is not actually used to allocate a buffer, but to 272 | // determine when a buffer must be flushed. 273 | trans.writeBufferSize = Math.pow(1024, 3); 274 | 275 | prot = new tprotocol.TBinaryProtocol(trans); 276 | 277 | tspan.write(prot); 278 | trans.flush(); 279 | }; 280 | 281 | if (typeof module !== 'undefined' && module.exports) { 282 | thrift = require('thrift'); 283 | zipkinCore_types = require('./_thrift/zipkinCore/zipkinCore_types'); 284 | ttransport = require('thrift/lib/thrift/transport'); 285 | tprotocol = require('thrift/lib/thrift/protocol'); 286 | Int64 = require('node-int64'); 287 | module.exports = { 288 | _hexStringify: hexStringify, 289 | formatForRestkin: formatForRestkin, 290 | formatForZipkin: formatForZipkin, 291 | formatForQueryService: formatForQueryService 292 | }; 293 | } else { 294 | this._hexStringify = hexStringify; 295 | this.formatForRestkin = formatForRestkin; 296 | } 297 | }()); 298 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Rackspace Hosting, 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 | 15 | module.exports = { 16 | trace: require('./trace'), 17 | tracers: require('./tracers'), 18 | node_tracers: require('./node_tracers'), 19 | formatters: require('./formatters') 20 | }; 21 | -------------------------------------------------------------------------------- /lib/node_tracers.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Rackspace Hosting, 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 | 15 | /** 16 | * Tracers only for Node. 17 | * Note: Node tracers assume tryfer library is included in a long running 18 | * process. 19 | */ 20 | 21 | if (typeof module === 'undefined' || !module.exports) { 22 | return; 23 | } 24 | 25 | var util = require('util'); 26 | 27 | var request = require('request'); 28 | var async = require('async'); 29 | 30 | var EndAnnotationTracer = require('./tracers').EndAnnotationTracer; 31 | var formatters = require('./formatters'); 32 | 33 | /** 34 | * Buffer traces and defer recording until maxTraces have been received or 35 | * sendInterval has elapsed since the last trace was recorded. 36 | * 37 | * @param {Tracer} tracer Tracer provider to record buffered tracers to. 38 | * @param {Options} options Options object with the following keys: 39 | * - maxTraces - Number of traces to buffer before recording occurs (default: 40 | * 50). 41 | * - sendInterval - An average number of seconds which can pass after the last 42 | * trace has been sent to the backend before sending all the buffered traces 43 | * to the backend again (default: 10). 44 | */ 45 | function BufferingTracer(tracer, options) { 46 | options = options || {}; 47 | 48 | var self = this; 49 | 50 | this._tracer = tracer; 51 | this._maxTraces = options.maxTraces || 50; 52 | this._sendInterval = options.sendInterval ? (options.sendInterval * 1000) : 10 * 1000; 53 | this._lastSentTs = Date.now(); 54 | this._buffer = []; 55 | this._stopped = false; 56 | 57 | this._periodSendTimeoutId = setTimeout(this._periodicSendFunction.bind(this), 58 | this._sendInterval); 59 | } 60 | 61 | BufferingTracer.prototype.sendTraces = function(traces) { 62 | var buffer; 63 | 64 | this._buffer = this._buffer.concat(traces); 65 | 66 | if (this._buffer.length >= this._maxTraces) { 67 | buffer = this._buffer.slice(); 68 | this._buffer = []; 69 | 70 | // Flush the buffer in the next tick 71 | process.nextTick(this._sendTraces.bind(this, buffer)); 72 | } 73 | }; 74 | 75 | /** 76 | * Clear any outgoing timers and stop the tracer. 77 | */ 78 | BufferingTracer.prototype.stop = function() { 79 | clearTimeout(this._periodSendTimeoutId); 80 | this._stopped = true; 81 | }; 82 | 83 | BufferingTracer.prototype._periodicSendFunction = function() { 84 | var now = Date.now(), buffer; 85 | 86 | if (((now - this._sendInterval) > this._lastSentTs) && 87 | this._buffer.length >= 1) { 88 | buffer = this._buffer.slice(); 89 | this._buffer = []; 90 | this._sendTraces(buffer); 91 | } 92 | 93 | // Re-schedule itself 94 | this._periodSendTimeoutId = setTimeout(this._periodicSendFunction.bind(this), 95 | this._sendInterval); 96 | }; 97 | 98 | BufferingTracer.prototype._sendTraces = function(traces) { 99 | this._lastSentTs = Date.now(); 100 | this._tracer.sendTraces(traces); 101 | }; 102 | 103 | /** 104 | * A tracer that records to zipkin through the RESTkin http interface. 105 | * Requires a keystone client ({node-keystone-client}). 106 | * 107 | * This implementation posts all traces immediately and does not implement 108 | * buffering. 109 | * 110 | * @param {String} traceUrl The URL to the RESTkin endpoint including the 111 | * version. For example: https://example.com/v1.0 112 | * @param {keystone-client.KeystoneClient object} keystoneClient The 113 | * keystone client to use to authenticate against keystone to get a token 114 | * and tenant id. 115 | */ 116 | function RawRESTkinHTTPTracer(traceUrl, keystoneClient) { 117 | if (traceUrl.charAt(traceUrl.length - 1) === '/') { 118 | traceUrl = traceUrl.slice(0, -1); 119 | } 120 | 121 | this._traceUrl = traceUrl; 122 | this._keystoneClient = keystoneClient; 123 | 124 | EndAnnotationTracer.call(this, this.sendTraces); 125 | } 126 | 127 | util.inherits(RawRESTkinHTTPTracer, EndAnnotationTracer); 128 | 129 | RawRESTkinHTTPTracer.prototype.performRequest = function (token, tenantId, body, callback) { 130 | var url = this._traceUrl + '/' + tenantId + '/trace', headers; 131 | 132 | headers = { 133 | 'X-Auth-Token': token, 134 | 'X-Tenant-Id': tenantId, 135 | 'Content-Type': 'application/json' 136 | }; 137 | 138 | request.post({ 139 | url: url, 140 | headers: headers, 141 | body: body 142 | }, callback); 143 | }; 144 | 145 | RawRESTkinHTTPTracer.prototype.sendTraces = function(traces) { 146 | var self = this; 147 | 148 | async.waterfall([ 149 | this._keystoneClient.getTenantIdAndToken.bind(this._keystoneClient, {}), 150 | 151 | function formatTraces(result, callback) { 152 | formatters.formatForRestkin(traces, function(err, formattedTraces) { 153 | callback(err, result, formattedTraces); 154 | }); 155 | }, 156 | 157 | function makeRequest(result, formattedTraces, callback) { 158 | self.performRequest(result.token, result.tenantId, formattedTraces, callback); 159 | } 160 | ], 161 | 162 | function(err) { 163 | if (err) { 164 | console.log('Failed to send traces to backend: ' + err.toString()); 165 | } 166 | }); 167 | }; 168 | 169 | /** 170 | * A tracer that records to zipkin through the RESTkin http interface. 171 | * Requires a keystone client ({node-keystone-client}). 172 | * 173 | * This is equivalent to EndAnnotationTracer(BufferingTracer(RawRESTkinHTTPTracerTests())). 174 | * 175 | * @param {String} traceUrl The URL to the RESTkin endpoint including the 176 | * version. For example: https://example.com/v1.0 177 | * @param {keystone-client.KeystoneClient object} keystoneClient The 178 | * keystone client to use to authenticate against keystone to get a token 179 | * and tenant id. 180 | * @param {Object} options Options passed to the BufferingTracer constructor. 181 | */ 182 | function RESTkinHTTPTracer(traceUrl, keystoneClient, options) { 183 | var rawTracer = new module.exports.RawRESTkinHTTPTracer(traceUrl, keystoneClient); 184 | this._tracer = new module.exports.BufferingTracer(rawTracer, options); 185 | 186 | this.stop = this._tracer.stop.bind(this._tracer); 187 | 188 | EndAnnotationTracer.call(this, this.sendTraces); 189 | } 190 | 191 | util.inherits(RESTkinHTTPTracer, EndAnnotationTracer); 192 | 193 | RESTkinHTTPTracer.prototype.sendTraces = function(traces) { 194 | this._tracer.sendTraces(traces); 195 | }; 196 | 197 | /** 198 | * A tracer that records directly to Zipkin Query HTTP API. 199 | * 200 | * This implementation posts all annotations to /api/v1/spans 201 | * immediately and does not implement buffering of any sort. 202 | * 203 | * @param {String} traceUrl The URL to the Zipkin query endpoint. 204 | * For example: https://example.com. 205 | */ 206 | function RawZipkinQueryServiceHTTPTracer(traceUrl) { 207 | if (traceUrl.charAt(traceUrl.length - 1) === '/') { 208 | traceUrl = traceUrl.slice(0, -1); 209 | } 210 | 211 | this._traceUrl = traceUrl; 212 | 213 | EndAnnotationTracer.call(this, this.sendTraces); 214 | } 215 | 216 | util.inherits(RawZipkinQueryServiceHTTPTracer, EndAnnotationTracer); 217 | 218 | RawZipkinQueryServiceHTTPTracer.prototype.performRequest = function (body, callback) { 219 | var url = this._traceUrl + '/api/v1/spans'; 220 | 221 | var headers = { 222 | 'Content-Type': 'application/json' 223 | }; 224 | 225 | request.post({ 226 | url: url, 227 | headers: headers, 228 | body: body 229 | }, callback); 230 | }; 231 | 232 | RawZipkinQueryServiceHTTPTracer.prototype.sendTraces = function(traces) { 233 | var self = this; 234 | 235 | async.waterfall([ 236 | function formatTraces(callback) { 237 | formatters.formatForQueryService(traces, function(err, formattedTraces) { 238 | callback(err, formattedTraces); 239 | }); 240 | }, 241 | 242 | function makeRequest(formattedTraces, callback) { 243 | self.performRequest(formattedTraces, function (err, httpResp, body) { 244 | if (err || httpResp.statusCode >= 400) { 245 | return callback(err || body); 246 | } 247 | callback(); 248 | }); 249 | } 250 | ], 251 | 252 | function(err) { 253 | if (err) { 254 | console.log('Failed to send traces to backend: ' + err.toString()); 255 | } 256 | }); 257 | }; 258 | 259 | /** 260 | * A tracer that records directly to Zipkin Query HTTP API. 261 | * 262 | * This implementation posts all annotations to /api/v1/spans 263 | * immediately and does not implement buffering of any sort. 264 | * 265 | * @param {String} traceUrl The URL to the Zipkin query endpoint. 266 | * For example: https://example.com. 267 | * @param {Object} options Options passed to the BufferingTracer constructor. 268 | */ 269 | function ZipkinQueryServiceHTTPTracer (serviceUrl, options) { 270 | var rawTracer = new module.exports.RawZipkinQueryServiceHTTPTracer(serviceUrl); 271 | this._tracer = new module.exports.BufferingTracer(rawTracer, options); 272 | 273 | this.stop = this._tracer.stop.bind(this._tracer); 274 | 275 | EndAnnotationTracer.call(this, this.sendTraces); 276 | } 277 | 278 | util.inherits(ZipkinQueryServiceHTTPTracer, EndAnnotationTracer); 279 | 280 | ZipkinQueryServiceHTTPTracer.prototype.sendTraces = function (traces) { 281 | this._tracer.sendTraces(traces); 282 | }; 283 | 284 | /** 285 | * A tracer that records to zipkin through scribe. Requires a scribe 286 | * client ({node-scribe}). 287 | * 288 | * @param {scribe.Scribe object} scribeClient The client to use to write to 289 | * scribe 290 | * @param {String} category The category to use when writing to scribe - 291 | * defaults to "zipkin" if not passed 292 | */ 293 | function RawZipkinTracer(scribeClient, category) { 294 | this.scribeClient = scribeClient; 295 | this.category = (category) ? category : 'zipkin'; 296 | 297 | EndAnnotationTracer.call(this, this.sendTraces); 298 | } 299 | 300 | util.inherits(RawZipkinTracer, EndAnnotationTracer); 301 | 302 | RawZipkinTracer.prototype._sendTrace = function(tuple, callback) { 303 | var trace = tuple[0], annotations = tuple[1]; 304 | var self = this; 305 | 306 | formatters.formatForZipkin(trace, annotations, function (err, message) { 307 | // scribe.Scribe.send does not take a callback and returns immediately 308 | callback( 309 | err, 310 | (err === null ? self.scribeClient.send(self.category, message) : null)); 311 | }); 312 | }; 313 | 314 | RawZipkinTracer.prototype.sendTraces = function(traces) { 315 | var callback = function() {}; 316 | 317 | async.forEachLimit(traces, 10, this._sendTrace.bind(this), 318 | callback); 319 | }; 320 | 321 | /** 322 | * A tracer that records to zipkin through scribe. Requires a scribe 323 | * client ({node-scribe}). 324 | * 325 | * @param {scribe.Scribe object} scribeClient The client to use to write to 326 | * scribe 327 | * @param {String} category The category to use when writing to scribe - 328 | * defaults to "zipkin" if not passed 329 | */ 330 | function ZipkinTracer(scribeClient, category, options) { 331 | var rawTracer = new RawZipkinTracer(scribeClient, category); 332 | this._tracer = new BufferingTracer(rawTracer, options); 333 | 334 | this.stop = this._tracer.stop.bind(this._tracer); 335 | 336 | EndAnnotationTracer.call(this, this.sendTraces); 337 | } 338 | 339 | util.inherits(ZipkinTracer, EndAnnotationTracer); 340 | 341 | ZipkinTracer.prototype.sendTraces = function(traces) { 342 | this._tracer.sendTraces(traces); 343 | }; 344 | 345 | /** 346 | * A tracer that records to RESTkin through scribe. Requires a scribe 347 | * client ({node-scribe}). 348 | * 349 | * This implementation logs all annotations immediately and does not implement 350 | * buffering of any sort. 351 | * 352 | * @param {scribe.Scribe object} scribeClient The client to use to write to 353 | * scribe 354 | * @param {String} category The category to use when writing to scribe - 355 | * defaults to "zipkin" if not passed 356 | */ 357 | function RawRESTkinScribeTracer(scribeClient, category) { 358 | this.scribeClient = scribeClient; 359 | this.category = (category) ? category : 'restkin'; 360 | 361 | EndAnnotationTracer.call(this, this.sendTraces); 362 | } 363 | 364 | util.inherits(RawRESTkinScribeTracer, EndAnnotationTracer); 365 | 366 | RawRESTkinScribeTracer.prototype.sendTraces = function(traces) { 367 | var self = this; 368 | 369 | async.waterfall([ 370 | formatters.formatForRestkin.bind(null, traces), 371 | 372 | function send(formattedTraces, callback) { 373 | self.scribeClient.send(self.category, formattedTraces); 374 | callback(); 375 | } 376 | ], 377 | 378 | function(err) { 379 | if (err) { 380 | console.log('Failed to send traces to backend: ' + err.toString()); 381 | } 382 | }); 383 | }; 384 | 385 | /** 386 | * A tracer that records to RESTkin through scribe. Requires a scribe 387 | * client ({node-scribe}). 388 | * 389 | * This implementation logs all annotations immediately and does not implement 390 | * buffering of any sort. 391 | * 392 | * @param {scribe.Scribe object} scribeClient The client to use to write to 393 | * scribe 394 | * @param {String} category The category to use when writing to scribe - 395 | * defaults to "zipkin" if not passed 396 | * @param {Object} options Options passed to the BufferingTracer constructor. 397 | */ 398 | function RESTkinScribeTracer(scribeClient, category, options) { 399 | var rawTracer = new RawRESTkinScribeTracer(scribeClient, category); 400 | 401 | this._tracer = new module.exports.BufferingTracer(rawTracer, options); 402 | this.stop = this._tracer.stop.bind(this._tracer); 403 | 404 | EndAnnotationTracer.call(this, this.sendTraces); 405 | } 406 | 407 | util.inherits(RESTkinScribeTracer, EndAnnotationTracer); 408 | 409 | RESTkinScribeTracer.prototype.sendTraces = function(traces) { 410 | this._tracer.sendTraces(traces); 411 | }; 412 | 413 | 414 | module.exports.BufferingTracer = BufferingTracer; 415 | module.exports.RawRESTkinHTTPTracer = RawRESTkinHTTPTracer; 416 | module.exports.RESTkinHTTPTracer = RESTkinHTTPTracer; 417 | module.exports.RawZipkinTracer = RawZipkinTracer; 418 | module.exports.ZipkinTracer = ZipkinTracer; 419 | module.exports.RawRESTkinScribeTracer = RawRESTkinScribeTracer; 420 | module.exports.RESTkinScribeTracer = RESTkinScribeTracer; 421 | module.exports.ZipkinQueryServiceHTTPTracer = ZipkinQueryServiceHTTPTracer; 422 | module.exports.RawZipkinQueryServiceHTTPTracer = RawZipkinQueryServiceHTTPTracer; 423 | -------------------------------------------------------------------------------- /lib/trace.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Rackspace Hosting, 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 | 15 | (function () { 16 | var tracers, formatters, zipkinCore_types; 17 | var Trace, Annotation, Endpoint, Int64; 18 | var forEach, has, getUniqueId, getNowMicros, getHex64, isIPv4; 19 | 20 | forEach = function(obj, f){ 21 | Array.prototype.forEach.call(obj, f); 22 | }; 23 | 24 | has = function(obj, val){ 25 | return Object.hasOwnProperty.call(obj, val); 26 | }; 27 | 28 | getUniqueId = function () { 29 | var random = new Int64(Math.random() * 0x80000000, Math.random() * 0x100000000); 30 | return random.toOctetString(); 31 | }; 32 | 33 | /** 34 | * Get a hex representation of provided value 35 | * 36 | * @param {String|number} hex Ex: "0xffffeeeeffefefef", "ffffeeeeffefefef", 12342344543543 37 | * NOTE: 0x12342344543543 is NOT a valid input 38 | * 39 | * @returns {Int64|undefined} 40 | */ 41 | getHex64 = function (hex) { 42 | try { 43 | return new Int64(hex).toOctetString(); 44 | } catch (e) { 45 | return undefined; 46 | } 47 | }; 48 | 49 | /** 50 | * Return the current unix epoch timestamp in microseconds 51 | * @return {number} 52 | */ 53 | getNowMicros = function() { 54 | return Date.now() * 1000; 55 | }; 56 | 57 | /** 58 | * Returns true if an IP address is v4 dotted decimal 59 | * @param {String} address 60 | * @returns {Boolean} 61 | */ 62 | isIPv4 = function(address) { 63 | var net = require('net'); 64 | // Use native check first if possible 65 | if (typeof net.isIPv4 === 'function') { 66 | return net.isIPv4(address); 67 | } else { 68 | var octets = address.split('.'); 69 | if (octets.length === 4) { 70 | return octets.reduce(function (result, value) { 71 | var n = parseInt(value, 10); 72 | return result && (!isNaN(n) && n >= 0 && n <= 255); 73 | }); 74 | } 75 | } 76 | return false; 77 | }; 78 | 79 | /** 80 | * A Trace encapsulates information about the current span of this trace 81 | * and provides a mechanism for creating new child spans and recording 82 | * annotations for this span. It delegates to zero or more {Tracers} and 83 | * allows setting a default {Endpoint} to associate with {Annotation}s 84 | * 85 | * @param {String} name A string describing this span 86 | * @param {Hash} options A hash any or all or none of the following 87 | * params, for which values will be generated if not provided: 88 | * 89 | * @param {Number} options.traceId 64-bit integer identifying this trace 90 | * @param {Number} options.spanId 64-bit integer identifying this span 91 | * @param {Number} options.parentSpanId 64-bit integer identifying this trace's 92 | * parent span, or undefined. 93 | * @param {Boolean} options.sampled false if this trace should NOT be sampled 94 | * default behaviour is to be sampled 95 | * @param {Array} options._tracers Zero or more {Tracers} 96 | */ 97 | Trace = function(name, options) { 98 | var self = this; 99 | self.name = name; 100 | 101 | options = options || {}; 102 | 103 | if (options.debug === true) { 104 | this.debug = true; 105 | } 106 | 107 | ['traceId', 'spanId', 'parentSpanId'].forEach(function(idName) { 108 | if (has(options, idName) && options[idName] !== null && 109 | options[idName] !== undefined) { 110 | self[idName] = getHex64(options[idName]); 111 | } else if (idName !== 'parentSpanId') { 112 | self[idName] = getUniqueId(); 113 | } 114 | }); 115 | 116 | if (has(options, 'tracers') && options.tracers) { 117 | self._tracers = options.tracers; 118 | } else { 119 | self._tracers = tracers.getTracers(); 120 | } 121 | 122 | if (has(options, 'sampled')){ 123 | this.sampled = (options.sampled === true); 124 | } else { 125 | this.sampled = true; 126 | } 127 | }; 128 | 129 | /** 130 | * Create a new instance of this class derived from the current instance 131 | * such that: 132 | * child.trace_id == current.trace_id, and 133 | * child.parent_span_id == current.span_id 134 | * The child {Trace} will have a new unique {spanId} and if set, the 135 | * endpoint of the current {Trace} object. 136 | * 137 | * @param {String} name The name describing the new span represented by the 138 | * new child {Trace} object. 139 | */ 140 | Trace.prototype.child = function (name) { 141 | var optionalArgs = { 142 | traceId: this.traceId, 143 | parentSpanId: this.spanId, 144 | debug: this.debug, 145 | sampled : this.sampled, 146 | tracers: this._tracers 147 | }; 148 | var trace = new Trace(name, optionalArgs); 149 | trace.setEndpoint(this.endpoint); 150 | return trace; 151 | }; 152 | 153 | /** 154 | * Record an annotation for this {Trace}. This is the primary entry point 155 | * for associating {Annotation}s with {Trace}s. It will delegate actual 156 | * recording to zero or more {Tracer}s. 157 | * 158 | * @param {Annotation} annotation The annotation to associate 159 | */ 160 | Trace.prototype.record = function (annotation) { 161 | var self = this; 162 | 163 | if (annotation.endpoint === undefined && this.endpoint !== undefined) { 164 | annotation.endpoint = this.endpoint; 165 | } 166 | 167 | if (this.sampled){ 168 | forEach(this._tracers, function(tracer) { 169 | tracer.record([[self, [annotation]]]); 170 | }); 171 | } 172 | }; 173 | 174 | /** 175 | * Set a default {Endpoint} for the current {Trace}. All {Annotation}s 176 | * recorded after this {Endpoint} is set will use it, unless they provide 177 | * their own {Endpoint}. 178 | * 179 | * @param {Endpoint} endpoint The default endpoint for all 180 | * {Annotation}s associated with this Trace 181 | */ 182 | Trace.prototype.setEndpoint = function (endpoint) { 183 | this.endpoint = endpoint; 184 | }; 185 | 186 | /** 187 | * Returns a hash of HTTP headers for this trace (to be used for making http 188 | * requests) to a server that supports tracing 189 | * 190 | * These headers are based on the headers used by finagle's tracing 191 | * http Codec. 192 | * 193 | * https://github.com/twitter/finagle/blob/master/finagle-http/ 194 | * 195 | * Currently not implemented is X-B3-Flags. 196 | * Tryfer's underlying Trace implementation does not support flags 197 | * 198 | * @param {Object} headers Optional. The headers to add tracing headers to. 199 | * If not provided, will use a new empty hash. Either way, will return 200 | * said hash. 201 | */ 202 | Trace.prototype.toHeaders = function (headers) { 203 | var self = this; 204 | if (headers === undefined) { 205 | headers = {}; 206 | } 207 | headers['X-B3-TraceId'] = self.traceId; 208 | headers['X-B3-SpanId'] = self.spanId; 209 | if (self.parentSpanId !== undefined) { 210 | headers['X-B3-ParentSpanId'] = self.parentSpanId; 211 | } 212 | if (self.sampled){ 213 | headers['X-B3-Sampled'] = 1; 214 | } else { 215 | headers['X-B3-Sampled'] = 0; 216 | } 217 | return headers; 218 | }; 219 | 220 | /** 221 | * Creates a new {Trace} given a hash of HTTP headers (to be used when 222 | * dealing with an incoming http request on a server that supports tracing). 223 | * 224 | * These headers are based on the headers used by finagle's tracing 225 | * http Codec. 226 | * 227 | * Currently not implemented is X-B3-Flags. 228 | * Tryfer's underlying Trace implementation does not support flags 229 | * 230 | * ASSUMPTION: header names are all lower cased 231 | * The default behavior of the NodeJS http module is to lower-case all 232 | * headers. Express (and it seems most other node http servers) also do 233 | * this, so we assume that all the headers are lower case 234 | * 235 | * @param {String} traceName What to name the trace - this can't come from 236 | * headers - it's highly recommended that this be the request method, to 237 | * match with tryfer's naming convention 238 | * @param {Object} headers A mapping of lower-cased header field names to 239 | * values 240 | * @param {Boolean} sampled Indicates local sampling decision. Inbound 241 | * X-B3-Sampled headers take precedence and will be honoured if present 242 | */ 243 | Trace.fromHeaders = function(traceName, headers, sampled) { 244 | var traceOptions = {}; 245 | 246 | ['X-B3-TraceId', 'X-B3-SpanId', 'X-B3-ParentSpanId'].forEach(function(v) { 247 | var processed = getHex64(headers[v.toLowerCase()]); 248 | if (processed !== undefined) { 249 | traceOptions[v.slice(5,6).toLowerCase() + v.slice(6)] = processed; 250 | } 251 | }); 252 | 253 | if (headers['x-b3-sampled'] === '0' || headers['x-b3-sampled'] === "false"){ 254 | traceOptions['sampled'] = false; 255 | } else if (headers['x-b3-sampled'] === '1' || headers['x-b3-sampled'] === "true"){ 256 | traceOptions['sampled'] = true; 257 | } else { 258 | traceOptions['sampled'] = sampled !== false; 259 | } 260 | 261 | return new Trace(traceName, traceOptions); 262 | }; 263 | 264 | /** 265 | * Creates a new {Trace} given a {http.ServerRequest}-like object. 266 | * Not only does this build a {Trace} based on the headers but sets the 267 | * server endpoint. The {http.ServerRequest}-like object should include at 268 | * least the following: 269 | * 270 | * - {request.method} - a {String} of the http method (e.g. GET, POST) - 271 | * defaults to GET 272 | * - {request.headers} - an {Object} containing the http headers - defaults 273 | * to an empty object 274 | * - {request.socket} - an {Object} containing information regarding the 275 | * http socket - this is used to determine the server endpoint, which 276 | * if socket is undefined will default to an IPv4 address of '127.0.0.1' 277 | * and a port of 80 278 | * 279 | * For more information on how the {Trace} is constructed from the headers, 280 | * see {Trace.fromHeaders} 281 | * 282 | * @param {Object} request The request object 283 | * @param {String} serviceName The name of the service) - defaults to 'http' 284 | * @param {Boolean} sampled - set to false if trace should NOT be sampled 285 | * defaults to true 286 | */ 287 | Trace.fromRequest = function(request, serviceName, sampled) { 288 | var newTrace = Trace.fromHeaders( 289 | request.method || 'GET', request.headers || {}, sampled); 290 | 291 | var host = (request.socket && request.socket.address ? 292 | request.socket.address() : { address: '127.0.0.1', port: 80 }); 293 | 294 | // Check IPv4 mapped IPv6 addresses 295 | if (/^::ffff:(\d{1,3}\.){3,3}\d{1,3}$/.test(host.address)) { 296 | host.address = host.address.replace(/^::ffff:/, ''); 297 | } 298 | 299 | if (host.address === '::1') { 300 | host.address = '127.0.0.1'; 301 | } else if (!isIPv4(host.address)) { 302 | host.address = '0.0.0.0'; 303 | } 304 | 305 | if (serviceName === undefined || serviceName === null) { 306 | serviceName = 'http'; 307 | } 308 | 309 | newTrace.setEndpoint(new Endpoint(host.address, host.port, serviceName)); 310 | return newTrace; 311 | }; 312 | 313 | 314 | /** 315 | * An IEndpoint represents a source of annotations in a distributed system. 316 | * 317 | * An endpoint represents the place where an event represented by an 318 | * annotation occurs. 319 | * 320 | * In a simple client/server RPC system both the client and server will 321 | * record Annotations for the same trace & span but those annotations will 322 | * have separate endpoints. On the client the endpoint will represent the 323 | * client service and on the server the endpoint will represent server 324 | * service. 325 | * 326 | * @param {String} ipv4 Dotted decimal string IP Address of this {Endpoint} 327 | * @param {number} port Integer port of this {Endpoint} 328 | * @param {String} serviceName Name of the service for this {Endpoint} 329 | */ 330 | Endpoint = function(ipv4, port, serviceName) { 331 | this.ipv4 = ipv4; 332 | this.port = parseInt(port, 10); 333 | this.serviceName = serviceName; 334 | }; 335 | 336 | 337 | /** 338 | * An annotation represents a piece of information attached to a trace. 339 | * 340 | * Most commonly this will be an event like: 341 | * * Client send 342 | * * Server receive 343 | * * Server send 344 | * * Client receive 345 | * 346 | * It may however also include non-event information such as the URI of 347 | * an HTTP request being made, or the user id that initiated the action 348 | * which caused the operations being traced to be performed. 349 | * 350 | * @param {String} name The name of this annotation 351 | * @param {number|String} value The value of this annotation 352 | * @param {String} annotationType A string describing the type of this 353 | * annotation. Should be one of: 354 | * {zipkinCore_types.CLIENT_SEND}, 355 | * {zipkinCore_types.CLIENT_RECV}, 356 | * {zipkinCore_types.SERVER_SEND}, 357 | * zipkinCore_types.CLIENT_RECV, 358 | * or String} annotationType 359 | * @param {Endpoint} endpoint 360 | * @param {number} duration of the event in microseconds (timestamp only) - optional 361 | * 362 | * NOTE: If the annotationType is NOT a timestamp, the value must be a string 363 | * (for now) 364 | */ 365 | Annotation = function(name, value, annotationType, endpoint, duration) { 366 | var self = this; 367 | self.name = name; 368 | self.value = value; 369 | self.annotationType = annotationType; 370 | if (endpoint !== undefined) { 371 | self.endpoint = endpoint; 372 | } 373 | if (duration !== undefined) { 374 | self.duration = duration; 375 | } 376 | }; 377 | 378 | /** 379 | * Creates a timestamp annotation, which represents an event. 380 | * 381 | * @param {String} name The name of this timestamp 382 | * @param {number} timestamp The time of the event in microseconds since the 383 | * epoch (UTC) - optional 384 | * @param {number} duration of the event in microseconds - optional 385 | */ 386 | Annotation.timestamp = function (name, timestamp, duration) { 387 | if (timestamp === undefined) { 388 | timestamp = getNowMicros(); 389 | } 390 | return new Annotation(name, timestamp, 'timestamp', undefined, duration); 391 | }; 392 | 393 | /** 394 | * Creates a client send (timestamp) annotation 395 | * 396 | * @param {number} timestamp The time of the client send event in 397 | * microseconds since the epoch (UTC) - optional 398 | */ 399 | Annotation.clientSend = function (timestamp) { 400 | return Annotation.timestamp(zipkinCore_types.CLIENT_SEND, timestamp); 401 | }; 402 | 403 | /** 404 | * Creates a client receive (timestamp) annotation 405 | * 406 | * @param {number} timestamp The time of the client receive event in 407 | * microseconds since the epoch (UTC) - optional 408 | */ 409 | Annotation.clientRecv = function (timestamp) { 410 | return Annotation.timestamp(zipkinCore_types.CLIENT_RECV, timestamp); 411 | }; 412 | 413 | /** 414 | * Creates a server send (timestamp) annotation 415 | * 416 | * @param {number} [timestamp] The time of the server send event in 417 | * microseconds since the epoch (UTC) - optional 418 | */ 419 | Annotation.serverSend = function (timestamp) { 420 | return Annotation.timestamp(zipkinCore_types.SERVER_SEND, timestamp); 421 | }; 422 | 423 | /** 424 | * Creates a server receive (timestamp) annotation 425 | * 426 | * @param {number} [timestamp] The time of the server receive event in 427 | * microseconds since the epoch (UTC) - optional 428 | */ 429 | Annotation.serverRecv = function (timestamp) { 430 | return Annotation.timestamp(zipkinCore_types.SERVER_RECV, timestamp); 431 | }; 432 | 433 | /** 434 | * Creates a unicode string (non-event) annotation 435 | * 436 | * @param {String} name The name of this string annotation 437 | * @param {String} value The string value of the annotation 438 | */ 439 | Annotation.string = function (name, value) { 440 | return new Annotation(name, value, 'string'); 441 | }; 442 | 443 | /** 444 | * Creates a uri annotation for client requests. This has the Annotation 445 | * name 'http.uri' because that is the standard set forth in the finagle 446 | * http Codec. 447 | * 448 | * @param {String} uri The URI to annotate 449 | */ 450 | Annotation.uri = function(uri) { 451 | return Annotation.string('http.uri', uri); 452 | }; 453 | 454 | 455 | if (typeof module !== 'undefined' && module.exports) { 456 | try { 457 | var microtime = require('microtime'); 458 | getNowMicros = function() { 459 | return microtime.now(); 460 | }; 461 | } catch (err) { 462 | // Use default implementation if microtime unavailable 463 | } 464 | tracers = require('./tracers'); 465 | formatters = require('./formatters'); 466 | Int64 = require('node-int64'); 467 | zipkinCore_types = require('./_thrift/zipkinCore/zipkinCore_types'); 468 | exports.Trace = Trace; 469 | exports.Endpoint = Endpoint; 470 | exports.Annotation = Annotation; 471 | // Exported only for tests 472 | exports._overrideGetNowMicros = function(func) { 473 | getNowMicros = func; 474 | }; 475 | } else { 476 | this.Trace = Trace; 477 | this.Endpoint = Endpoint; 478 | this.Annotation = Annotation; 479 | } 480 | 481 | }()); 482 | -------------------------------------------------------------------------------- /lib/tracers.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Rackspace Hosting, 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 | 15 | /** 16 | Tracers are responsible for collecting and delivering annotations and traces. 17 | 18 | Traces are expected to be delivered asynchronously and ITracer.record 19 | is not expected to wait until a Trace has been successfully delivered before 20 | returning to it's caller. Given the asynchronous nature of trace delivery any 21 | errors which occur as a result of attempting to deliver a trace MUST be 22 | handled by the Tracer. 23 | 24 | A Tracer has a function: record, which records an annotation for the 25 | specified trace. 26 | */ 27 | 28 | (function () { 29 | var getTracers, pushTracer, setTracers, DebugTracer, EndAnnotationTracer; 30 | var formatters; 31 | var zipkinCore_types; 32 | 33 | var globalTracers = []; 34 | 35 | var has = function(obj, val){ 36 | return Object.hasOwnProperty.call(obj, val); 37 | }; 38 | 39 | /** 40 | * Tracer that writes to a stream 41 | * 42 | * @param {Stream} destination Stream to write annotations to 43 | */ 44 | DebugTracer = function (destination) { 45 | var self = this; 46 | self.destination = destination; 47 | }; 48 | 49 | /** 50 | * Writes an annotation for the specified trace to stdout 51 | * 52 | * @param {Array} traces Array of [trace, annotations] tuples. 53 | */ 54 | DebugTracer.prototype.record = function (traces) { 55 | var self = this; 56 | 57 | traces.forEach(function(tuple) { 58 | var trace, annotations; 59 | 60 | trace = tuple[0]; 61 | annotations = tuple[1]; 62 | 63 | formatters.formatForRestkin([[trace, annotations]], function(err, json) { 64 | self.destination.write('--- Trace ---\n'); 65 | // pretty print the json 66 | self.destination.write(json); 67 | self.destination.write('\n'); 68 | }); 69 | }); 70 | }; 71 | 72 | /** 73 | * Tracer that writes to a stream 74 | * 75 | * @param {function} sendTracesCallback Callback that takes a {Trace} and an 76 | * {Array} of {Annotation}, and sends it to zipkin 77 | */ 78 | EndAnnotationTracer = function (sendTracesCallback) { 79 | var self = this; 80 | // keeps track of annotations per span, but since span ID is not 81 | // guaranteed to be globally unique, tracks span IDs per trace ID 82 | self.annotationsForSpan = {}; 83 | self.sendTraces = sendTracesCallback; 84 | }; 85 | 86 | /** 87 | * Records a trace and annotation, and if the annotation is an end annotation 88 | * sends it off to zipkin 89 | * 90 | * @param {Array} traces Array of [trace, annotations] tuples. 91 | */ 92 | EndAnnotationTracer.prototype.record = function (traces) { 93 | var self = this; 94 | 95 | traces.forEach(function(tuple) { 96 | var trace, annotations, readyToGo; 97 | 98 | trace = tuple[0]; 99 | annotations = tuple[1]; 100 | 101 | if (!has(self.annotationsForSpan, trace.traceId)) { 102 | self.annotationsForSpan[trace.traceId] = {}; 103 | } 104 | 105 | if (!has(self.annotationsForSpan[trace.traceId], trace.spanId)) { 106 | self.annotationsForSpan[trace.traceId][trace.spanId] = []; 107 | } 108 | 109 | self.annotationsForSpan[trace.traceId][trace.spanId] = self.annotationsForSpan[trace.traceId][trace.spanId].concat(annotations); 110 | 111 | annotations.forEach(function(annotation) { 112 | // if it's an end annotation - ship it 113 | if (annotation.name === zipkinCore_types.CLIENT_RECV || 114 | annotation.name === zipkinCore_types.SERVER_SEND) { 115 | readyToGo = self.annotationsForSpan[trace.traceId][trace.spanId]; 116 | 117 | delete self.annotationsForSpan[trace.traceId][trace.spanId]; 118 | 119 | // Should probably log a message with trace and annotations here 120 | if (self.sendTraces !== undefined) { 121 | self.sendTraces([[trace, readyToGo]]); 122 | } 123 | } 124 | }); 125 | 126 | if (Object.keys(self.annotationsForSpan[trace.traceId]).length === 0) { 127 | delete self.annotationsForSpan[trace.traceId]; 128 | } 129 | }); 130 | }; 131 | 132 | if (typeof module !== 'undefined' && module.exports) { 133 | zipkinCore_types = require('./_thrift/zipkinCore/zipkinCore_types'); 134 | formatters = require('./formatters'); 135 | module.exports = { 136 | DebugTracer: DebugTracer, 137 | EndAnnotationTracer: EndAnnotationTracer, 138 | getTracers: function () { return globalTracers; }, 139 | pushTracer: function (tracer) { globalTracers.push(tracer); }, 140 | setTracers: function (tracers) { globalTracers = [].concat(tracers); } 141 | }; 142 | } else { 143 | this.DebugTracer = DebugTracer; 144 | this.EndAnnotationTracer = EndAnnotationTracer; 145 | this.getTracers = getTracers; 146 | this.pushTracer = pushTracer; 147 | this.setTracers = setTracers; 148 | } 149 | }()); 150 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Rackspace US, Inc.", 3 | "name": "tryfer", 4 | "description": "Port of the tryfer Twisted/Python tracing library and Zipkin client", 5 | "version": "0.2.13", 6 | "main": "./index", 7 | "contributors": [ 8 | { 9 | "name": "Ying Li", 10 | "email": "ying.li@rackspace.com", 11 | "url": "https://github.com/cyli" 12 | }, 13 | { 14 | "name": "Matt Kaniaris", 15 | "email": "matthew.kaniaris@rackspace.com", 16 | "url": "http://kansface.com" 17 | }, 18 | { 19 | "name": "Tomaz Muraus", 20 | "email": "tomaz.muraus@rackspace.com", 21 | "url": "http://www.tomaz.me" 22 | } 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/racker/node-tryfer" 27 | }, 28 | "dependencies": { 29 | "thrift": "0.9.2", 30 | "async": "0.1.22", 31 | "request": "2.74.0", 32 | "node-int64": "0.3.3" 33 | }, 34 | "devDependencies": { 35 | "express": ">= 3.2.6 < 3.3", 36 | "nodeunit": "", 37 | "underscore": "1.3.x", 38 | "espresso": "", 39 | "jshint": ">= 2.1.3 <= 2.2" 40 | }, 41 | "optionalDependencies": { 42 | "keystone-client": ">= 0.3.0", 43 | "microtime": ">= 1.0.0", 44 | "scribe": "" 45 | }, 46 | "engines": { 47 | "node": ">= 0.6.0 < 0.11" 48 | }, 49 | "scripts": { 50 | "test": "./bin/tester", 51 | "lint": "./bin/lint.sh" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /scripts/tryfer_format_sample.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python script that produces base64 and json formats of a trace and a 3 | set of annotations, so they can be tested in the node port. 4 | """ 5 | 6 | from tryfer import formatters 7 | from tryfer.trace import Trace, Endpoint, Annotation 8 | 9 | trace = Trace('trace', trace_id=100, span_id=10, parent_span_id=5) 10 | endpoint = Endpoint('1.2.3.4', 8080, 'myservice') 11 | annotations = [Annotation.timestamp('mytime', 1), 12 | Annotation.string('mystring', 'value')] 13 | for annotation in annotations: 14 | annotation.endpoint = endpoint 15 | 16 | print formatters.base64_thrift_formatter(trace, annotations) 17 | print formatters.json_formatter(trace, annotations) 18 | -------------------------------------------------------------------------------- /tests/test_formatters.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Rackspace Hosting, 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 | 15 | var util = require('util'); 16 | 17 | var Int64 = require('node-int64'); 18 | var _ = require('underscore'); 19 | var tprotocol = require('../node_modules/thrift/lib/thrift/protocol'); 20 | var ttransport = require('../node_modules/thrift/lib/thrift/transport'); 21 | 22 | var formatters = require('..').formatters; 23 | var trace = require('..').trace; 24 | var zipkinCore_types = require('../lib/_thrift/zipkinCore/zipkinCore_types'); 25 | 26 | // The test cases for the formatters - basic formatting of trace with 27 | // timestamp annotation and other annotation, one with a parentSpanId in the 28 | // trace, and one with an endpoint in one of the annotations 29 | var testcases = { 30 | basic_trace_and_annotations: { 31 | trace: new trace.Trace('test', {spanId: 10, traceId:1}), 32 | annotations: [new trace.Annotation.timestamp('name1', 1), 33 | new trace.Annotation.string('name2', '2')] 34 | }, 35 | trace_with_optional_params: { 36 | trace: new trace.Trace('test', {spanId: 10, traceId:1, debug: true}), 37 | annotations: [new trace.Annotation.timestamp('name1', 1, 123), 38 | new trace.Annotation.string('name2', '2')] 39 | }, 40 | trace_with_parentSpanId: { 41 | trace: new trace.Trace('test', {parentSpanId:5, spanId: 10, traceId:1}), 42 | annotations: [] 43 | }, 44 | trace_with_annotation_with_endpoint: { 45 | trace: new trace.Trace('test', {spanId: 10, traceId:1}), 46 | annotations: [ 47 | new trace.Annotation( 48 | 'name1', 1, 'timestamp', (new trace.Endpoint('1.1.1.1', 5, 'service')) 49 | )] 50 | } 51 | }; 52 | 53 | // Helper function to test formatting for Reskin - takes a test object, 54 | // one of the test cases from above, and the expected object represented 55 | // by the formatted json string 56 | var testRestkinFormatter = function (test, testcase, expected) { 57 | formatters.formatForRestkin([[testcase.trace, testcase.annotations]], 58 | function(err, jsonStr) { 59 | test.equal(err, null); 60 | test.deepEqual(JSON.parse(jsonStr), expected); 61 | test.done(); 62 | }); 63 | }; 64 | 65 | // Helper function to test formatting for Zipkin - takes a test object, 66 | // one of the test cases from above, and the expected zipkinCore_types object 67 | // represented base64 encoded string 68 | var testZipkinFormatter = function (test, testcase, expected) { 69 | formatters.formatForZipkin(testcase.trace, testcase.annotations, 70 | function(formatError, base64str) { 71 | var b = new Buffer(base64str, "base64"); 72 | var trans_receiver = ttransport.TBufferedTransport.receiver(function(trans) { 73 | var prot = new tprotocol.TBinaryProtocol(trans); 74 | var span = new zipkinCore_types.Span(); 75 | span.read(prot); 76 | _.each(span.binary_annotations, function (annotation) { 77 | if (annotation.value) { 78 | annotation.value = annotation.value.toString(); 79 | } 80 | }); 81 | test.deepEqual(span, expected); 82 | test.done(); 83 | }); 84 | 85 | test.equal(formatError, null); 86 | trans_receiver(b); 87 | }); 88 | }; 89 | 90 | 91 | // Helper function to test formatting for Zipkin Query API - takes a test object, 92 | // one of the test cases from above, and the expected object represented 93 | // by the formatted json string 94 | var testZipkinQueryApiFormatter = function (test, testcase, expected) { 95 | formatters.formatForQueryService([[testcase.trace, testcase.annotations]], 96 | function (formatError, jsonStr) { 97 | test.equal(formatError, null); 98 | test.deepEqual(JSON.parse(jsonStr), expected); 99 | test.done(); 100 | }); 101 | }; 102 | 103 | module.exports = { 104 | restkinFormatterTests: { 105 | test_basic_trace_and_annotations: function(test){ 106 | testRestkinFormatter(test, testcases.basic_trace_and_annotations, [{ 107 | trace_id: '0000000000000001', 108 | span_id: '000000000000000a', 109 | name: 'test', 110 | annotations: [ 111 | { 112 | key: 'name1', 113 | value: 1, 114 | type: 'timestamp' 115 | }, 116 | { 117 | key: 'name2', 118 | value: '2', 119 | type: 'string' 120 | } 121 | ] 122 | }]); 123 | }, 124 | test_trace_with_optional_params: function (test) { 125 | testRestkinFormatter(test, testcases.trace_with_optional_params, [{ 126 | trace_id: '0000000000000001', 127 | span_id: '000000000000000a', 128 | name: 'test', 129 | debug: true, 130 | annotations: [ 131 | { 132 | key: 'name1', 133 | value: 1, 134 | type: 'timestamp', 135 | duration: 123 136 | }, 137 | { 138 | key: 'name2', 139 | value: '2', 140 | type: 'string' 141 | } 142 | ] 143 | }]); 144 | }, 145 | test_trace_with_parentSpanId: function(test){ 146 | testRestkinFormatter(test, testcases.trace_with_parentSpanId, [{ 147 | trace_id: '0000000000000001', 148 | parent_span_id: '0000000000000005', 149 | span_id: '000000000000000a', 150 | name: 'test', 151 | annotations: [] 152 | }]); 153 | }, 154 | test_trace_with_annotation_with_endpoint: function(test) { 155 | testRestkinFormatter( 156 | test, testcases.trace_with_annotation_with_endpoint, [{ 157 | trace_id: '0000000000000001', 158 | span_id: '000000000000000a', 159 | name: 'test', 160 | annotations: [ 161 | { 162 | key: 'name1', 163 | value: 1, 164 | type: 'timestamp', 165 | host: { 166 | ipv4: '1.1.1.1', 167 | port: 5, 168 | service_name: 'service' 169 | } 170 | } 171 | ] 172 | }]); 173 | } 174 | }, 175 | zipkinFormatterTests: { 176 | test_basic_trace_and_annotations: function(test){ 177 | testZipkinFormatter( 178 | test, testcases.basic_trace_and_annotations, 179 | new zipkinCore_types.Span({ 180 | trace_id: new Int64(1), 181 | id: new Int64(10), 182 | name: 'test', 183 | annotations: [ new zipkinCore_types.Annotation({ 184 | timestamp: new Int64(1), 185 | value: 'name1' 186 | })], 187 | binary_annotations: [ new zipkinCore_types.BinaryAnnotation({ 188 | value: '2', 189 | key: 'name2', 190 | annotation_type: zipkinCore_types.AnnotationType.STRING 191 | })] 192 | })); 193 | }, 194 | test_trace_with_optional_params: function(test){ 195 | testZipkinFormatter( 196 | test, testcases.trace_with_optional_params, 197 | new zipkinCore_types.Span({ 198 | trace_id: new Int64(1), 199 | id: new Int64(10), 200 | name: 'test', 201 | debug: true, 202 | annotations: [ new zipkinCore_types.Annotation({ 203 | timestamp: new Int64(1), 204 | value: 'name1', 205 | duration: 123 206 | })], 207 | binary_annotations: [ new zipkinCore_types.BinaryAnnotation({ 208 | value: '2', 209 | key: 'name2', 210 | annotation_type: zipkinCore_types.AnnotationType.STRING 211 | })] 212 | })); 213 | }, 214 | test_trace_with_parentSpanId: function(test){ 215 | testZipkinFormatter( 216 | test, testcases.trace_with_parentSpanId, 217 | new zipkinCore_types.Span({ 218 | trace_id: new Int64(1), 219 | parent_id: new Int64(5), 220 | id: new Int64(10), 221 | name: 'test', 222 | annotations: [], 223 | binary_annotations: [] 224 | })); 225 | }, 226 | test_trace_with_annotation_with_endpoint: function(test) { 227 | testZipkinFormatter( 228 | test, testcases.trace_with_annotation_with_endpoint, 229 | new zipkinCore_types.Span({ 230 | trace_id: new Int64(1), 231 | id: new Int64(10), 232 | name: 'test', 233 | annotations: [ new zipkinCore_types.Annotation({ 234 | timestamp: new Int64(1), 235 | value: 'name1', 236 | host: new zipkinCore_types.Endpoint({ 237 | // formula is (first octet * 256^3) + (second octet * 256^2) + 238 | // (third octet * 256) + (fourth octet) 239 | ipv4: Math.pow(256, 3) + Math.pow(256, 2) + 256 + 1, 240 | port: 5, 241 | service_name: 'service' 242 | }) 243 | })], 244 | binary_annotations: [] 245 | })); 246 | } 247 | }, 248 | zipkinQueryServiceApiFormatter: { 249 | test_basic_trace_and_annotations: function (test) { 250 | testZipkinQueryApiFormatter( 251 | test, testcases.basic_trace_and_annotations, [{ 252 | traceId: '0000000000000001', 253 | id: '000000000000000a', 254 | name: 'test', 255 | annotations: [ 256 | { 257 | 'timestamp' : 1, 258 | 'value' : 'name1' 259 | } 260 | ], 261 | binaryAnnotations: [ 262 | { 263 | key: 'name2', 264 | value: '2', 265 | type: 'string' 266 | } 267 | ] 268 | }]); 269 | }, 270 | test_trace_with_parentSpanId: function(test){ 271 | testZipkinQueryApiFormatter( 272 | test, testcases.trace_with_parentSpanId, [{ 273 | traceId: '0000000000000001', 274 | parentId: '0000000000000005', 275 | id: '000000000000000a', 276 | name: 'test', 277 | annotations: [], 278 | binaryAnnotations: [] 279 | }]); 280 | }, 281 | test_trace_with_annotation_with_endpoint: function(test) { 282 | testZipkinQueryApiFormatter( 283 | test, testcases.trace_with_annotation_with_endpoint, [{ 284 | traceId: '0000000000000001', 285 | id: '000000000000000a', 286 | name: 'test', 287 | annotations: [ 288 | { 289 | value: 'name1', 290 | timestamp: 1, 291 | endpoint: { 292 | ipv4: '1.1.1.1', 293 | port: 5, 294 | serviceName: 'service' 295 | } 296 | } 297 | ], 298 | binaryAnnotations: [] 299 | }]); 300 | } 301 | } 302 | }; 303 | -------------------------------------------------------------------------------- /tests/test_node_tracers.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Rackspace Hosting, 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 | 15 | var trace = require('..').trace; 16 | var node_tracers = require('..').node_tracers; 17 | 18 | var express = require('express'); 19 | var http = require('http'); 20 | var util = require('util'); 21 | 22 | var mockKeystoneClient = { 23 | getTenantIdAndToken: function(options, cb) { 24 | cb(null, {'token': '1', 'expires': 2, 'tenantId': '3'}); 25 | } 26 | }; 27 | 28 | // Asserts (via a nodeunit test object) that the result is a base64 string 29 | var assert_is_base64 = function(test, string) { 30 | test.ok(string.search(/^([A-Za-z0-9+\/]{4})*([A-Za-z0-9+\/]{4}|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{2}==)$/) >= 0); 31 | }; 32 | 33 | // Asserts (via a nodeunit test object) that the result is a json array - if 34 | // it is a string representing a json array, parses the string 35 | var assert_is_json_array = function(test, json) { 36 | if (typeof json === typeof "") { 37 | json = JSON.parse(json); 38 | } 39 | test.ok(util.isArray(json)); 40 | test.ok(json.length >= 1); 41 | }; 42 | 43 | // A fake scribe client for testing purposes - rather than send data to scribe, 44 | // it saves the category and message sent to it and allows assertions to be 45 | // made on the data (via a nodeunit test object) 46 | var FakeScribe = function(test) { 47 | var self = this; 48 | self.test = test; 49 | self.results = []; 50 | self.send = function(category, message) { 51 | self.results.push([category, message]); 52 | }; 53 | self.assert_sent = function() { 54 | self.test.equal(self.results.length, 1); 55 | self.test.equal(self.results[0].length, 2); 56 | }; 57 | self.assert_category = function(category) { 58 | self.test.equal(self.results[0][0], category); 59 | }; 60 | self.assert_json = function() { 61 | assert_is_json_array(self.test, self.results[0][1]); 62 | }; 63 | self.assert_base64 = function() { 64 | assert_is_base64(self.test, self.results[0][1]); 65 | }; 66 | }; 67 | 68 | // Creates n traces 69 | var createTraces = function(n) { 70 | var i; 71 | var traces = []; 72 | 73 | for (i = 0; i < n; i++) { 74 | traces.push( 75 | [new trace.Trace('clientRecv'), [trace.Annotation.clientRecv(2)]]); 76 | } 77 | 78 | return traces; 79 | }; 80 | 81 | 82 | module.exports = { 83 | setUp: function(cb) { 84 | this.traces = createTraces(1); 85 | cb(); 86 | }, 87 | 88 | test_raw_restkin_tracer: function(test){ 89 | var self = this; 90 | var app = express(); 91 | var server = http.createServer(app); 92 | var tracer1, tracer2; 93 | var i = 0; 94 | 95 | app.use(express.bodyParser()); 96 | 97 | app.post('/3/trace', function(request, response) { 98 | i++; 99 | test.equal(request.headers['x-auth-token'], '1'); 100 | test.equal(request.headers['x-tenant-id'], '3'); 101 | test.equal(request.headers['content-type'], 'application/json'); 102 | test.notEqual(request.headers['content-length'], '0'); 103 | 104 | assert_is_json_array(test, request.body); 105 | test.equal(request.body.length, 1); 106 | 107 | response.end('done'); 108 | 109 | if (i === 2) { 110 | server.close(); 111 | } 112 | }); 113 | 114 | server.on('close', function(){ 115 | test.done(); 116 | }); 117 | 118 | server.listen(22222, 'localhost'); 119 | 120 | // Valid URL 121 | tracer1 = new node_tracers.RawRESTkinHTTPTracer('http://localhost:22222', 122 | mockKeystoneClient); 123 | 124 | // Valid URL with trailing slash 125 | tracer2 = new node_tracers.RawRESTkinHTTPTracer('http://localhost:22222/', 126 | mockKeystoneClient); 127 | 128 | tracer1.record(self.traces); 129 | tracer2.record(self.traces); 130 | }, 131 | 132 | test_raw_restkin_tracer_server_connection_to_server_refused: function(test) { 133 | var tracer; 134 | 135 | tracer = new node_tracers.RawRESTkinHTTPTracer('http://localhost:1898/', 136 | mockKeystoneClient); 137 | 138 | tracer.record(this.traces); 139 | test.done(); 140 | }, 141 | 142 | test_restkin_tracer_maxTraces: function(test) { 143 | var self = this; 144 | var app = express(); 145 | var server = http.createServer(app); 146 | var tracer; 147 | var options; 148 | var i; 149 | 150 | app.use(express.bodyParser()); 151 | 152 | app.post('/3/trace', function(request, response) { 153 | var body = request.body; 154 | 155 | test.equal(request.headers['x-auth-token'], '1'); 156 | test.equal(request.headers['x-tenant-id'], '3'); 157 | test.equal(request.headers['content-type'], 'application/json'); 158 | test.notEqual(request.headers['content-length'], '0'); 159 | 160 | assert_is_json_array(test, body); 161 | test.equal(body.length, 15); 162 | 163 | response.end('done'); 164 | tracer.stop(); 165 | server.close(); 166 | }); 167 | 168 | server.on('close', function(){ 169 | test.done(); 170 | }); 171 | 172 | server.listen(22222, 'localhost'); 173 | 174 | options = {'maxTraces': 15}; 175 | tracer = new node_tracers.RESTkinHTTPTracer('http://localhost:22222', 176 | mockKeystoneClient, options); 177 | 178 | for (i = 0; i < 15; i++) { 179 | tracer.record(this.traces); 180 | } 181 | }, 182 | 183 | test_restkin_tracer_sendInterval: function(test) { 184 | var self = this; 185 | var app = express(); 186 | var server = http.createServer(app); 187 | var tracer; 188 | var options; 189 | var now = Date.now(); 190 | 191 | app.use(express.bodyParser()); 192 | 193 | app.post('/3/trace', function(request, response) { 194 | var body = request.body; 195 | 196 | test.equal(request.headers['x-auth-token'], '1'); 197 | test.equal(request.headers['x-tenant-id'], '3'); 198 | test.equal(request.headers['content-type'], 'application/json'); 199 | test.notEqual(request.headers['content-length'], '0'); 200 | 201 | assert_is_json_array(test, body); 202 | test.equal(body.length, 2); 203 | test.ok(now + 1000 <= Date.now()); 204 | 205 | response.end('done'); 206 | server.close(); 207 | tracer.stop(); 208 | }); 209 | 210 | server.on('close', function(){ 211 | test.done(); 212 | }); 213 | 214 | server.listen(22222, 'localhost'); 215 | 216 | options = {'sendInterval': 1}; 217 | tracer = new node_tracers.RESTkinHTTPTracer('http://localhost:22222', 218 | mockKeystoneClient, options); 219 | 220 | tracer.record(this.traces); 221 | tracer.record(this.traces); 222 | }, 223 | 224 | test_zipkin_tracer_default_category: function(test){ 225 | var s = new FakeScribe(test); 226 | var t = new node_tracers.RawZipkinTracer(s); 227 | t.record(this.traces); 228 | 229 | setTimeout(function() { 230 | s.assert_sent(); 231 | s.assert_category('zipkin'); 232 | s.assert_base64(); 233 | test.done(); 234 | }, 100); 235 | }, 236 | 237 | test_zipkin_tracer_provided_category: function(test){ 238 | var s = new FakeScribe(test); 239 | var t = new node_tracers.RawZipkinTracer(s, 'mycategory'); 240 | t.record(this.traces); 241 | 242 | setTimeout(function() { 243 | s.assert_sent(); 244 | s.assert_category('mycategory'); 245 | s.assert_base64(); 246 | test.done(); 247 | }, 100); 248 | }, 249 | 250 | /* Since RawZipkinTracer sends in batches of 10, ensure that if there are 251 | * more than 10 traces all of them get sent. 252 | */ 253 | test_zipkin_tracer_over_10_traces: function(test) { 254 | var s = new FakeScribe(test); 255 | var t = new node_tracers.RawZipkinTracer(s); 256 | t.sendTraces(createTraces(15)); 257 | 258 | setTimeout(function() { 259 | test.equal(s.results.length, 15); 260 | test.done(); 261 | }, 100); 262 | }, 263 | 264 | test_restkin_scribe_tracer_default_category: function(test){ 265 | var s = new FakeScribe(test); 266 | var t = new node_tracers.RawRESTkinScribeTracer(s); 267 | t.record(this.traces); 268 | 269 | setTimeout(function() { 270 | s.assert_sent(); 271 | s.assert_category('restkin'); 272 | s.assert_json(); 273 | test.done(); 274 | }, 100); 275 | }, 276 | 277 | test_restkin_scribe_tracer_provided_category: function(test){ 278 | var s = new FakeScribe(test); 279 | var t = new node_tracers.RawRESTkinScribeTracer(s, 'mycategory'); 280 | t.record(this.traces); 281 | 282 | setTimeout(function() { 283 | s.assert_sent(); 284 | s.assert_category('mycategory'); 285 | s.assert_json(); 286 | test.done(); 287 | }, 100); 288 | }, 289 | 290 | test_raw_zipkin_query_tracer: function(test){ 291 | var self = this; 292 | var app = express(); 293 | var server = http.createServer(app); 294 | var tracer1, tracer2; 295 | var i = 0; 296 | 297 | app.use(express.bodyParser()); 298 | 299 | app.post('/api/v1/spans', function(request, response) { 300 | i++; 301 | test.equal(request.headers['content-type'], 'application/json'); 302 | test.notEqual(request.headers['content-length'], '0'); 303 | 304 | assert_is_json_array(test, request.body); 305 | test.equal(request.body.length, 1); 306 | 307 | response.end('done'); 308 | 309 | if (i === 2) { 310 | server.close(); 311 | } 312 | }); 313 | 314 | server.on('close', function(){ 315 | test.done(); 316 | }); 317 | 318 | server.listen(22222, 'localhost'); 319 | 320 | // Valid URL 321 | tracer1 = new node_tracers.RawZipkinQueryServiceHTTPTracer('http://localhost:22222'); 322 | 323 | // Valid URL with trailing slash 324 | tracer2 = new node_tracers.RawZipkinQueryServiceHTTPTracer('http://localhost:22222/'); 325 | 326 | tracer1.record(self.traces); 327 | tracer2.record(self.traces); 328 | }, 329 | 330 | test_raw_zipkin_query_tracer_server_connection_to_server_refused: function(test) { 331 | var tracer; 332 | 333 | tracer = new node_tracers.RawZipkinQueryServiceHTTPTracer('http://localhost:1898/'); 334 | 335 | tracer.record(this.traces); 336 | test.done(); 337 | }, 338 | 339 | test_zipkin_query_maxTraces: function(test) { 340 | var self = this; 341 | var app = express(); 342 | var server = http.createServer(app); 343 | var tracer; 344 | var options; 345 | var i; 346 | 347 | app.use(express.bodyParser()); 348 | 349 | app.post('/api/v1/spans', function(request, response) { 350 | var body = request.body; 351 | 352 | test.equal(request.headers['content-type'], 'application/json'); 353 | test.notEqual(request.headers['content-length'], '0'); 354 | 355 | assert_is_json_array(test, body); 356 | test.equal(body.length, 15); 357 | 358 | response.end('done'); 359 | tracer.stop(); 360 | server.close(); 361 | }); 362 | 363 | server.on('close', function(){ 364 | test.done(); 365 | }); 366 | 367 | server.listen(22222, 'localhost'); 368 | 369 | options = {'maxTraces': 15}; 370 | tracer = new node_tracers.ZipkinQueryServiceHTTPTracer('http://localhost:22222', options); 371 | 372 | for (i = 0; i < 15; i++) { 373 | tracer.record(this.traces); 374 | } 375 | }, 376 | 377 | test_zipkin_query_sendInterval: function(test) { 378 | var self = this; 379 | var app = express(); 380 | var server = http.createServer(app); 381 | var tracer; 382 | var options; 383 | var now = Date.now(); 384 | 385 | app.use(express.bodyParser()); 386 | 387 | app.post('/api/v1/spans', function(request, response) { 388 | var body = request.body; 389 | 390 | test.equal(request.headers['content-type'], 'application/json'); 391 | test.notEqual(request.headers['content-length'], '0'); 392 | 393 | assert_is_json_array(test, body); 394 | test.equal(body.length, 2); 395 | test.ok(now + 1000 <= Date.now()); 396 | 397 | response.end('done'); 398 | server.close(); 399 | tracer.stop(); 400 | }); 401 | 402 | server.on('close', function(){ 403 | test.done(); 404 | }); 405 | 406 | server.listen(22222, 'localhost'); 407 | 408 | options = {'sendInterval': 1}; 409 | tracer = new node_tracers.ZipkinQueryServiceHTTPTracer('http://localhost:22222', options); 410 | 411 | tracer.record(this.traces); 412 | tracer.record(this.traces); 413 | } 414 | }; 415 | -------------------------------------------------------------------------------- /tests/test_trace.js: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Rackspace Hosting, 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 | 15 | var util = require('util'); 16 | 17 | var ass = require('nodeunit').assert; 18 | 19 | var trace = require('..').trace; 20 | var tracers = require('..').tracers; 21 | var formatters = require('..').formatters; 22 | 23 | var MAX_ID = 'ffffffffffffffff'; 24 | 25 | ass.isNum = function(num){ 26 | ass.equal(isNaN(num), false, 27 | util.format("%s is not number like", num)); 28 | }; 29 | 30 | var mockTracer = function(name, id, endpoint){ 31 | var self = this; 32 | self.name = name; 33 | self.id = id; 34 | self.endpoint = endpoint; 35 | self._calls = {record: []}; 36 | self.record = function(){ 37 | self._calls.record.push(Array.prototype.slice.call(arguments)); 38 | }; 39 | }; 40 | 41 | // a valid trace has a traceId and a spanId that are numbers that are greater 42 | // than or equal to zero and less than the max id 43 | var assert_is_valid_trace = function(test, t) { 44 | test.notEqual(t.traceId, undefined); 45 | test.ok(typeof t.traceId === 'string'); 46 | test.ok(t.traceId < MAX_ID); 47 | test.ok(t.traceId.length === 16); 48 | 49 | test.notEqual(t.spanId, undefined); 50 | test.ok(typeof t.traceId === 'string'); 51 | test.ok(t.spanId < MAX_ID); 52 | test.ok(t.spanId.length === 16); 53 | }; 54 | 55 | // generate an Annotation test 56 | var runAnnotationTest = function(test, ann, name, value, ann_type, duration) { 57 | test.equal(ann.name, name); 58 | test.equal(ann.value, value); 59 | test.equal(ann.annotationType, ann_type); 60 | test.equal(ann.duration, duration); 61 | test.done(); 62 | }; 63 | 64 | module.exports = { 65 | traceTests: { 66 | setUp: function(cb){ 67 | tracers.setTracers([]); 68 | cb(); 69 | }, 70 | tearDown: function(cb){ 71 | tracers.setTracers([]); 72 | cb(); 73 | }, 74 | test_new_trace: function(test){ 75 | var t = new trace.Trace('test_trace'); 76 | test.equal(t.name, 'test_trace'); 77 | test.equal(t.parentSpanId, undefined); 78 | test.equal(t.debug, undefined); 79 | assert_is_valid_trace(test, t); 80 | test.done(); 81 | }, 82 | test_new_trace_debug: function(test){ 83 | var t = new trace.Trace('test_trace', {debug: true}); 84 | test.equal(t.name, 'test_trace'); 85 | test.equal(t.parentSpanId, undefined); 86 | test.equal(t.debug, true); 87 | assert_is_valid_trace(test, t); 88 | test.done(); 89 | }, 90 | test_trace_child: function(test){ 91 | var t = new trace.Trace('test_trace', {traceId: 1, spanId: 1}); 92 | var c = t.child('child_test_trace'); 93 | test.equal(c.traceId, 1); 94 | test.equal(c.parentSpanId, 1); 95 | test.equal(c.debug, undefined); 96 | test.done(); 97 | }, 98 | test_trace_child_debug: function(test){ 99 | var t = new trace.Trace('test_trace', {traceId: 1, spanId: 1, debug: true}); 100 | var c = t.child('child_test_trace'); 101 | test.equal(c.traceId, 1); 102 | test.equal(c.parentSpanId, 1); 103 | test.equal(c.debug, true); 104 | test.done(); 105 | }, 106 | test_trace_child_passes_tracers: function(test){ 107 | var o = {traceId: 1, spanId: 1, tracers: [1]}; 108 | var t = new trace.Trace('test_trace', o); 109 | var c = t.child('child_test_trace'); 110 | test.equal(c.traceId, 1); 111 | test.equal(c.parentSpanId, 1); 112 | test.equal(c._tracers[0], 1); 113 | test.done(); 114 | }, 115 | test_record_invokes_tracer: function(test){ 116 | var tracer, t, a; 117 | tracer = new mockTracer(); 118 | t = new trace.Trace('test_trace', { 119 | traceId: 1, spanId: 1, tracers: [tracer] 120 | }); 121 | a = trace.Annotation.clientSend(0); 122 | t.record(a); 123 | test.deepEqual(tracer._calls.record[0], [[[t, [a]]]]); 124 | test.done(); 125 | }, 126 | test_record_does_not_invoke_tracer_when_not_sampled: function(test){ 127 | var tracer, t, a; 128 | tracer = new mockTracer(); 129 | t = new trace.Trace('test_trace', { 130 | traceId: 1, spanId: 1, tracers: [tracer], sampled: false 131 | }); 132 | a = trace.Annotation.clientSend(0); 133 | t.record(a); 134 | 135 | test.equal(tracer._calls.record.length, 0); 136 | test.done(); 137 | }, 138 | test_record_sets_annotation_endpoint: function(test){ 139 | var tracer, e, t, a; 140 | tracer = new mockTracer(); 141 | e = new trace.Endpoint('127.0.0.1', 8080, 'web'); 142 | t = new trace.Trace('test_trace', 143 | {trace_id: 1, span_id: 1, tracers: [tracer]}); 144 | t.setEndpoint(e); 145 | a = new trace.Annotation.clientSend(1); 146 | t.record(a); 147 | test.deepEqual(tracer._calls.record[0], [[[t, [a]]]]); 148 | test.deepEqual(a.endpoint, e); 149 | test.done(); 150 | }, 151 | test_toHeaders_no_parent_span: function(test) { 152 | var t = new trace.Trace('GET', {traceId: 1, spanId: 10}); 153 | test.deepEqual(t.toHeaders(), { 154 | 'X-B3-TraceId': '0000000000000001', 155 | 'X-B3-SpanId': '000000000000000a', 156 | 'X-B3-Sampled' : '1' 157 | }); 158 | test.done(); 159 | }, 160 | test_toHeaders_parent_span: function(test) { 161 | var t = new trace.Trace('GET', {traceId: 1, spanId: 10, parentSpanId: 5}); 162 | test.deepEqual(t.toHeaders(), { 163 | 'X-B3-TraceId': '0000000000000001', 164 | 'X-B3-SpanId': '000000000000000a', 165 | 'X-B3-ParentSpanId': '0000000000000005', 166 | 'X-B3-Sampled' : '1' 167 | }); 168 | test.done(); 169 | }, 170 | test_toHeaders_add_to_existing_header: function(test) { 171 | var t = new trace.Trace('GET', {traceId: 1, spanId: 10}); 172 | var headers = {'Content-Type': 'application/json'}; 173 | test.deepEqual(t.toHeaders(headers), { 174 | 'X-B3-TraceId': '0000000000000001', 175 | 'X-B3-SpanId': '000000000000000a', 176 | 'X-B3-Sampled' : '1', 177 | 'Content-Type': 'application/json' 178 | }); 179 | test.done(); 180 | }, 181 | test_fromHeaders_emtpy_headers: function(test) { 182 | // tests to make sure that without header ids, a valid trace is 183 | // still produced 184 | var t = trace.Trace.fromHeaders('GET', {}); 185 | test.equal(t.name, 'GET'); 186 | assert_is_valid_trace(test, t); 187 | test.done(); 188 | }, 189 | test_fromHeaders_without_parent_id: function(test) { 190 | var t = trace.Trace.fromHeaders('POST', 191 | { 192 | 'x-b3-traceid': '000000000000000a', 193 | 'x-b3-spanid': '000000000000000a' 194 | }); 195 | 196 | test.equal(t.name, 'POST'); 197 | test.equal(t.traceId, formatters._hexStringify(10)); 198 | test.equal(t.spanId, formatters._hexStringify(10)); 199 | test.equal(t.parentSpanId, undefined); 200 | test.done(); 201 | }, 202 | test_fromHeaders_with_parent_id: function(test) { 203 | var t = trace.Trace.fromHeaders('POST', 204 | { 205 | 'x-b3-traceid': '0000000000000001', 206 | 'x-b3-spanid': '000000000000000a', 207 | 'x-b3-parentspanid': '0000000000000005' 208 | }); 209 | 210 | test.equal(t.name, 'POST'); 211 | test.equal(t.traceId, formatters._hexStringify(1)); 212 | test.equal(t.spanId, formatters._hexStringify(10)); 213 | test.equal(t.parentSpanId, formatters._hexStringify(5)); 214 | test.done(); 215 | }, 216 | test_fromHeaders_with_sampled: function(test) { 217 | var t = trace.Trace.fromHeaders('POST', 218 | { 219 | 'x-b3-traceid': '0000000000000001', 220 | 'x-b3-spanid': '000000000000000a', 221 | 'x-b3-sampled': '0' 222 | }); 223 | 224 | test.equal(t.name, 'POST'); 225 | test.equal(t.traceId, formatters._hexStringify(1)); 226 | test.equal(t.spanId, formatters._hexStringify(10)); 227 | test.equal(t.sampled, false); 228 | test.done(); 229 | }, 230 | test_fromRequest_calls_fromHeaders: function(test) { 231 | var orig = trace.Trace.fromHeaders; 232 | var test_headers = {'hat': 'cat'}; 233 | trace.Trace.fromHeaders = function(traceName, headers) { 234 | try { 235 | test.equal(traceName, 'POST'); 236 | test.equal(headers, test_headers); // should be the same reference 237 | } finally { 238 | // always clean up 239 | trace.Trace.fromHeaders = orig; 240 | } 241 | return trace.Trace.fromHeaders(traceName, headers); 242 | }; 243 | trace.Trace.fromRequest({method: 'POST', headers: test_headers}); 244 | test.done(); 245 | }, 246 | test_fromRequest_defaults_sampled_behaviour_to_sampled : function(test){ 247 | var request = { 248 | 'headers' : { 249 | 'x-b3-traceid': '0000000000000001', 250 | 'x-b3-spanid': '000000000000000a' 251 | } 252 | }; 253 | var t = trace.Trace.fromRequest(request, 'test'); 254 | test.equal(t.sampled, true); 255 | test.done(); 256 | }, 257 | test_fromRequest_honours_inbound_dont_sample_header_as_binary_over_local_do_sample_indicator : function(test){ 258 | var request = { 259 | 'headers' : { 260 | 'x-b3-traceid': '0000000000000001', 261 | 'x-b3-spanid': '000000000000000a', 262 | 'x-b3-sampled': '0' 263 | } 264 | }; 265 | var t = trace.Trace.fromRequest(request, 'test', true); 266 | test.equal(t.sampled, false); 267 | test.done(); 268 | }, 269 | test_fromRequest_honours_inbound_dont_sample_header_as_boolean_over_local_do_sample_indicator : function(test){ 270 | var request = { 271 | 'headers' : { 272 | 'x-b3-traceid': '0000000000000001', 273 | 'x-b3-spanid': '000000000000000a', 274 | 'x-b3-sampled': 'false' 275 | } 276 | }; 277 | var t = trace.Trace.fromRequest(request, 'test', true); 278 | test.equal(t.sampled, false); 279 | test.done(); 280 | }, 281 | test_fromRequest_honours_inbound_do_sample_header_as_binary_over_local_dont_sample_indicator : function(test){ 282 | var request = { 283 | 'headers' : { 284 | 'x-b3-traceid': '0000000000000001', 285 | 'x-b3-spanid': '000000000000000a', 286 | 'x-b3-sampled': '1' 287 | } 288 | }; 289 | var t = trace.Trace.fromRequest(request, 'test', false); 290 | test.equal(t.sampled, true); 291 | test.done(); 292 | }, 293 | test_fromRequest_honours_inbound_do_sample_header_as_boolean_over_local_dont_sample_indicator : function(test){ 294 | var request = { 295 | 'headers' : { 296 | 'x-b3-traceid': '0000000000000001', 297 | 'x-b3-spanid': '000000000000000a', 298 | 'x-b3-sampled': 'true' 299 | } 300 | }; 301 | var t = trace.Trace.fromRequest(request, 'test', false); 302 | test.equal(t.sampled, true); 303 | test.done(); 304 | }, 305 | test_fromRequest_honours_local_sampled_indicator_in_the_absence_of_an_inbound_sampled_header : function(test){ 306 | var request = { 307 | 'headers' : { 308 | 'x-b3-traceid': '0000000000000001', 309 | 'x-b3-spanid': '000000000000000a' 310 | } 311 | }; 312 | var t = trace.Trace.fromRequest(request, 'test', false); 313 | test.equal(t.sampled, false); 314 | test.done(); 315 | }, 316 | test_fromRequest_default_endpoint: function(test) { 317 | var t = trace.Trace.fromRequest({method: 'GET', headers: {}}); 318 | test.equal(t.endpoint.ipv4, '127.0.0.1'); 319 | test.equal(t.endpoint.port, 80); 320 | test.equal(t.endpoint.serviceName, "http"); 321 | test.done(); 322 | }, 323 | test_fromRequest_endpoint_from_address_info: function(test) { 324 | var t = new trace.Trace.fromRequest({ 325 | method: 'GET', 326 | headers: {}, 327 | socket: { 328 | address: function() { 329 | return {family: 2, port: 8888, address: '1.2.3.4'}; 330 | } 331 | } 332 | }); 333 | test.equal(t.endpoint.ipv4, '1.2.3.4'); 334 | test.equal(t.endpoint.port, 8888); 335 | test.done(); 336 | }, 337 | test_fromRequest_endpoint_with_servicename: function(test) { 338 | var t = new trace.Trace.fromRequest( 339 | {method: 'GET', headers: {}}, 'this_is_a_service'); 340 | test.equal(t.endpoint.serviceName, "this_is_a_service"); 341 | test.done(); 342 | }, 343 | test_fromRequest_endpoint_with_ipv6_localhost: function(test) { 344 | var t = new trace.Trace.fromRequest({ 345 | method: 'GET', 346 | headers: {}, 347 | socket: { 348 | address: function() { 349 | return {family: 2, port: 8888, address: '::1'}; 350 | } 351 | } 352 | }); 353 | test.equal(t.endpoint.ipv4, '127.0.0.1'); 354 | test.equal(t.endpoint.port, 8888); 355 | test.done(); 356 | }, 357 | test_fromRequest_endpoint_with_ipv6_mapped_localhost: function(test) { 358 | var t = new trace.Trace.fromRequest({ 359 | method: 'GET', 360 | headers: {}, 361 | socket: { 362 | address: function() { 363 | return {family: 2, port: 8888, address: '::ffff:127.0.0.1'}; 364 | } 365 | } 366 | }); 367 | test.equal(t.endpoint.ipv4, '127.0.0.1'); 368 | test.equal(t.endpoint.port, 8888); 369 | test.done(); 370 | }, 371 | test_fromRequest_endpoint_with_other_ipv6: function(test) { 372 | var t = new trace.Trace.fromRequest({ 373 | method: 'GET', 374 | headers: {}, 375 | socket: { 376 | address: function() { 377 | return {family: 2, port: 8888, address: '2001:db8::ff00:42:8329'}; 378 | } 379 | } 380 | }); 381 | test.equal(t.endpoint.ipv4, '0.0.0.0'); 382 | test.equal(t.endpoint.port, 8888); 383 | test.done(); 384 | } 385 | }, 386 | annotationTests: { 387 | setUp: function(cb){ 388 | trace._overrideGetNowMicros(function() { 389 | return 1000000; 390 | }); 391 | cb(); 392 | }, 393 | test_timestamp: function(test) { 394 | runAnnotationTest(test, trace.Annotation.timestamp('test'), 'test', 395 | 1000000, 'timestamp'); 396 | }, 397 | test_timestamp_with_duration: function(test) { 398 | runAnnotationTest(test, trace.Annotation.timestamp('test', undefined, 123), 'test', 399 | 1000000, 'timestamp', 123); 400 | }, 401 | test_client_send: function(test) { 402 | runAnnotationTest(test, trace.Annotation.clientSend(), 'cs', 1000000, 403 | 'timestamp'); 404 | }, 405 | test_client_recv: function(test) { 406 | runAnnotationTest(test, trace.Annotation.clientRecv(), 'cr', 1000000, 407 | 'timestamp'); 408 | }, 409 | test_server_send: function(test) { 410 | runAnnotationTest(test, trace.Annotation.serverSend(), 'ss', 1000000, 411 | 'timestamp'); 412 | }, 413 | test_server_recv: function(test) { 414 | runAnnotationTest(test, trace.Annotation.serverRecv(), 'sr', 1000000, 415 | 'timestamp'); 416 | }, 417 | test_string: function(test) { 418 | runAnnotationTest(test, trace.Annotation.string('myname', 'ispi'), 419 | 'myname', 'ispi', 'string'); 420 | }, 421 | test_uri: function(test) { 422 | var uri = 'http://example.com'; 423 | runAnnotationTest(test, trace.Annotation.uri(uri), 'http.uri', uri, 424 | 'string'); 425 | } 426 | } 427 | }; 428 | -------------------------------------------------------------------------------- /tests/test_tracers.js: -------------------------------------------------------------------------------- 1 | var tracers = require('..').tracers; 2 | var trace = require('..').trace; 3 | var formatters = require('..').formatters; 4 | 5 | module.exports = { 6 | globalFunctions: { 7 | test_set_tracers: function(test){ 8 | var dummy_tracer = ["a"]; 9 | tracers.setTracers(dummy_tracer); 10 | test.deepEqual(tracers.getTracers(), dummy_tracer); 11 | test.done(); 12 | }, 13 | 14 | test_push_tracer: function(test){ 15 | var dummy_tracer = "1"; 16 | var dummy_tracer2 = "2"; 17 | 18 | tracers.pushTracer(dummy_tracer); 19 | test.deepEqual(tracers.getTracers(), [dummy_tracer]); 20 | tracers.pushTracer(dummy_tracer2); 21 | test.deepEqual(tracers.getTracers(), 22 | [dummy_tracer, dummy_tracer2]); 23 | test.done(); 24 | }, 25 | 26 | test_set_trace_args: function(test){ 27 | var dummy_tracer = ["i'm a tracer"]; 28 | tracers.setTracers(dummy_tracer); 29 | test.deepEqual(tracers.getTracers(), dummy_tracer); 30 | 31 | dummy_tracer = 2; 32 | tracers.setTracers(dummy_tracer); 33 | test.deepEqual(tracers.getTracers(), [dummy_tracer]); 34 | test.done(); 35 | }, 36 | 37 | setUp: function(cb){ 38 | tracers.setTracers([]); 39 | cb(); 40 | }, 41 | 42 | tearDown: function(cb){ 43 | tracers.setTracers([]); 44 | cb(); 45 | } 46 | }, 47 | 48 | debugTracer: { 49 | test_writes_to_stream: function(test){ 50 | // setup 51 | var written = '', debug_tracer, t, a, traces; 52 | var mock_stream = { 53 | write: function(data) { written += data; } 54 | }; 55 | 56 | debug_tracer = new tracers.DebugTracer(mock_stream); 57 | t = new trace.Trace('test', {traceId: 1, spanId:2, parentSpanId:1}); 58 | a = new trace.Annotation.timestamp('mytime', 100); 59 | traces = [[t, [a]]]; 60 | debug_tracer.record(traces); 61 | 62 | formatters.formatForRestkin(traces, function(err, json) { 63 | var expected = '--- Trace ---\n' + JSON.stringify( 64 | JSON.parse(json), null, 2) + '\n'; 65 | test.equal(written, expected); 66 | test.done(); 67 | }); 68 | } 69 | }, 70 | 71 | endAnnotationTracer: { 72 | setUp: function(cb){ 73 | var self = this; 74 | self.sent_traces = []; 75 | self.tracer = new tracers.EndAnnotationTracer( 76 | function(traces) { 77 | self.sent_traces.push(traces); 78 | }); 79 | self.non_end_annotations = [ 80 | trace.Annotation.timestamp('mytime', 1), 81 | trace.Annotation.clientSend(1), 82 | trace.Annotation.serverRecv(1), 83 | trace.Annotation.string("myname", "myval") 84 | ]; 85 | cb(); 86 | }, 87 | 88 | test_record_non_end_annotations_does_not_send: function(test) { 89 | var self = this; 90 | var t = new trace.Trace('mytrace'); 91 | self.non_end_annotations.forEach(function(annotation) { 92 | self.tracer.record([[t, [annotation]]]); 93 | test.equal(self.sent_traces.length, 0); 94 | }); 95 | test.done(); 96 | }, 97 | 98 | test_record_sends_all_stored_on_end_annotation: function(test) { 99 | var self = this; 100 | var t = new trace.Trace('mytrace'); 101 | var end_annotation = trace.Annotation.clientRecv(2); 102 | self.non_end_annotations.forEach(function(annotation) { 103 | self.tracer.record([[t, [annotation]]]); 104 | }); 105 | 106 | self.tracer.record([[t, [end_annotation]]]); 107 | test.equal(self.sent_traces.length, 1); 108 | test.deepEqual(self.sent_traces[0][0], 109 | [t, self.non_end_annotations.concat([end_annotation])]); 110 | test.done(); 111 | }, 112 | 113 | test_record_sends_on_clientRecv_and_serverSend: function(test) { 114 | var self = this; 115 | self.tracer.record([[new trace.Trace('clientRecv'), 116 | [trace.Annotation.clientRecv(2)]]]); 117 | self.tracer.record([[new trace.Trace('serverSend'), 118 | [trace.Annotation.serverSend(2)]]]); 119 | test.equal(self.sent_traces.length, 2); 120 | test.done(); 121 | }, 122 | 123 | test_record_for_child_spans: function(test) { 124 | var self = this; 125 | var span = new trace.Trace('server'); 126 | self.tracer.record([[span, 127 | [trace.Annotation.serverRecv(2)]]]); 128 | self.tracer.record([[span, 129 | [trace.Annotation.string("http.uri", "/test", 2)]]]); 130 | 131 | var child = span.child("upstream"); 132 | self.tracer.record([[child, 133 | [trace.Annotation.string("http.uri", "/internal/test", 2)]]]); 134 | self.tracer.record([[child, 135 | [trace.Annotation.clientSend(2)]]]); 136 | self.tracer.record([[child, 137 | [trace.Annotation.clientRecv(2)]]]); 138 | 139 | self.tracer.record([[span, 140 | [trace.Annotation.serverSend(2)]]]); 141 | 142 | test.equal(self.sent_traces.length, 2); 143 | // Check that we end up with 3 annotations for each span 144 | test.equal(self.sent_traces[0][0][1].length, 3); 145 | test.equal(self.sent_traces[1][0][1].length, 3); 146 | test.done(); 147 | }, 148 | 149 | test_annotation_records_do_not_grow_infinitely: function(test) { 150 | var self = this; 151 | var t = new trace.Trace('mytrace'); 152 | 153 | test.equal(Object.keys(self.tracer.annotationsForSpan).length, 0); 154 | 155 | self.tracer.record([[t, [trace.Annotation.clientSend(2)]]]); 156 | test.equal(Object.keys(self.tracer.annotationsForSpan).length, 1); 157 | test.equal( 158 | Object.keys(self.tracer.annotationsForSpan[t.traceId]).length, 159 | 1); 160 | 161 | self.tracer.record([[t, [trace.Annotation.clientRecv(2)]]]); 162 | 163 | test.equal(Object.keys(self.tracer.annotationsForSpan).length, 0); 164 | 165 | test.done(); 166 | } 167 | } 168 | }; 169 | -------------------------------------------------------------------------------- /tests/test_tryfer_compatibility.js: -------------------------------------------------------------------------------- 1 | /* 2 | Node script that produces base64 and json formats of a trace and a 3 | set of annotations. Tests against the base64 and json formats produced by 4 | the original tryfer (by running against results that are produced by 5 | scripts/tryfer_format_sample.py) 6 | */ 7 | 8 | var _ = require('underscore'); 9 | 10 | var formatters = require('..').formatters; 11 | var trace = require('..').trace; 12 | 13 | var sample_trace = new trace.Trace('trace', { 14 | traceId: 100, spanId: 10, parentSpanId: 5}); 15 | var endpoint = new trace.Endpoint('1.2.3.4', 8080, 'myservice'); 16 | var sample_annotations = [new trace.Annotation.timestamp('mytime', 1), 17 | new trace.Annotation.string('mystring', 'value')]; 18 | sample_annotations.forEach(function(ann) { 19 | ann.endpoint = endpoint; 20 | }); 21 | 22 | var expected_base64 = "CgABAAAAAAAAAGQLAAMAAAAFdHJhY2UKAAQAAAAAAAAACgoABQAAAAAAAAAFDwAGDAAAAAEKAAEAAAAAAAAAAQsAAgAAAAZteXRpbWUMAAMIAAEBAgMEBgACH5ALAAMAAAAJbXlzZXJ2aWNlAAAPAAgMAAAAAQsAAQAAAAhteXN0cmluZwsAAgAAAAV2YWx1ZQgAAwAAAAYMAAQIAAEBAgMEBgACH5ALAAMAAAAJbXlzZXJ2aWNlAAAA"; 23 | 24 | var expected_json = '[{"annotations": [{"host": {"service_name": "myservice", "ipv4": "1.2.3.4", "port": 8080}, "type": "timestamp", "value": 1, "key": "mytime"}, {"host": {"service_name": "myservice", "ipv4": "1.2.3.4", "port": 8080}, "type": "string", "value": "value", "key": "mystring"}], "parent_span_id": "0000000000000005", "trace_id": "0000000000000064", "name": "trace", "span_id": "000000000000000a"}]'; 25 | 26 | formatters.formatForZipkin(sample_trace, sample_annotations, 27 | function(error, value) { 28 | if (expected_base64 !== value) { 29 | console.log('expected: ' + expected_base64 + "\nresult: " + value); 30 | } 31 | }); 32 | formatters.formatForRestkin([[sample_trace, sample_annotations]], 33 | function(error, value) { 34 | var expected = JSON.parse(expected_json); 35 | var result = JSON.parse(value); 36 | if (!_.isEqual(result, expected)) { 37 | console.log('Expected:\n'); 38 | console.log(expected); 39 | console.log('\nResult:\n'); 40 | console.log(result); 41 | } 42 | }); 43 | --------------------------------------------------------------------------------