├── .gitignore ├── src ├── reporters │ ├── index.js │ ├── PrometheusReporter.js │ ├── PrometheusReporter.e2e.spec.js │ └── PrometheusReporter.spec.js ├── index.js └── tracer │ ├── index.js │ ├── Reference.js │ ├── SpanContext.js │ ├── SpanContext.spec.js │ ├── Reference.spec.js │ ├── Span.js │ ├── Tracer.spec.js │ ├── Tracer.js │ └── Span.spec.js ├── .travis.yml ├── test └── setup.js ├── .eslintrc.yml ├── LICENSE ├── package.json ├── example └── server.js ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | -------------------------------------------------------------------------------- /src/reporters/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const PrometheusReporter = require('./PrometheusReporter') 4 | 5 | module.exports = { 6 | PrometheusReporter 7 | } 8 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Tracer } = require('./tracer') 4 | const { PrometheusReporter } = require('./reporters') 5 | 6 | module.exports = Object.assign(Tracer, { 7 | PrometheusReporter 8 | }) 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - node_modules 5 | notifications: 6 | email: true 7 | node_js: 8 | - 'stable' 9 | - '8' 10 | branches: 11 | except: 12 | - /^v\d+\.\d+\.\d+$/ 13 | -------------------------------------------------------------------------------- /src/tracer/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Reference = require('./Reference') 4 | const Span = require('./Span') 5 | const SpanContext = require('./SpanContext') 6 | const Tracer = require('./Tracer') 7 | 8 | module.exports = { 9 | Reference, 10 | Span, 11 | SpanContext, 12 | Tracer 13 | } 14 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const sinon = require('sinon') 4 | const chai = require('chai') 5 | const sinonChai = require('sinon-chai') 6 | 7 | before(() => { 8 | chai.use(sinonChai) 9 | }) 10 | 11 | beforeEach(function beforeEach () { 12 | this.sandbox = sinon.sandbox.create() 13 | }) 14 | 15 | afterEach(function afterEach () { 16 | this.sandbox.restore() 17 | }) 18 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: airbnb-base 3 | env: 4 | node: true 5 | mocha: true 6 | es6: true 7 | parserOptions: 8 | sourceType: strict 9 | rules: 10 | generator-star-spacing: 11 | - 2 12 | - before: true 13 | after: true 14 | no-shadow: 0 15 | require-yield: 0 16 | no-param-reassign: 0 17 | comma-dangle: 18 | - error 19 | - never 20 | no-underscore-dangle: 0 21 | import/no-extraneous-dependencies: 22 | - 2 23 | - devDependencies: true 24 | import/order: 25 | - error 26 | func-names: 0 27 | no-unused-expressions: 0 28 | prefer-arrow-callback: 1 29 | no-use-before-define: 30 | - 2 31 | - functions: false 32 | space-before-function-paren: 33 | - 2 34 | - always 35 | max-len: 36 | - 2 37 | - 120 38 | - 2 39 | semi: 40 | - 2 41 | - never 42 | strict: 43 | - 2 44 | - global 45 | arrow-parens: 46 | - 2 47 | - always 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 RisingStack, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@risingstack/opentracing-metrics-tracer", 3 | "version": "2.1.0", 4 | "description": "Exports metrics via OpenTracing instrumentation and Prometheus reporter", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "mocha test/setup.js 'src/**/*.spec.js'", 8 | "coverage": "nyc --reporter=html --reporter=text npm run test", 9 | "lint": "eslint test src" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/RisingStack/opentracing-metrics-tracer.git" 14 | }, 15 | "author": "Peter Marton", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/RisingStack/opentracing-metrics-tracer/issues" 19 | }, 20 | "homepage": "https://github.com/RisingStack/opentracing-metrics-tracer#readme", 21 | "dependencies": { 22 | "opentracing": "0.14.1", 23 | "prom-client": "10.1.1", 24 | "uuid": "3.1.0" 25 | }, 26 | "devDependencies": { 27 | "chai": "4.1.2", 28 | "dedent": "0.7.0", 29 | "eslint": "4.7.2", 30 | "eslint-config-airbnb-base": "11.0.0", 31 | "eslint-plugin-import": "2.7.0", 32 | "eslint-plugin-promise": "3.5.0", 33 | "mocha": "3.5.3", 34 | "nyc": "11.2.1", 35 | "sinon": "3.2.1", 36 | "sinon-chai": "2.14.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/tracer/Reference.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('assert') 4 | const { REFERENCE_CHILD_OF, REFERENCE_FOLLOWS_FROM } = require('opentracing') 5 | const Span = require('./Span') 6 | const SpanContext = require('./SpanContext') 7 | 8 | /** 9 | * Reference pairs a reference type constant (e.g., REFERENCE_CHILD_OF or REFERENCE_FOLLOWS_FROM) 10 | * with the SpanContext it points to 11 | * Follows the original opentracing API 12 | * @class Reference 13 | */ 14 | class Reference { 15 | /** 16 | * @constructor 17 | * @param {String} type - the Reference type constant (e.g., REFERENCE_CHILD_OF or REFERENCE_FOLLOWS_FROM 18 | * @param {SpanContext|Span} referencedContext - the SpanContext being referred to. 19 | * As a convenience, a Span instance may be passed in instead (in which case its .context() is used here) 20 | * @returns {Reference} 21 | */ 22 | constructor (type, referencedContext) { 23 | assert([REFERENCE_CHILD_OF, REFERENCE_FOLLOWS_FROM].includes(type), 'Invalid type') 24 | assert( 25 | referencedContext instanceof Span || referencedContext instanceof SpanContext, 26 | 'referencedContext must have a type Span or SpanContext' 27 | ) 28 | 29 | this._type = type 30 | this._referencedContext = referencedContext instanceof Span ? 31 | referencedContext.context() : 32 | referencedContext 33 | } 34 | 35 | /** 36 | * @method referencedContext 37 | * @return {SpanContext} referencedContext 38 | */ 39 | referencedContext () { 40 | return this._referencedContext 41 | } 42 | 43 | /** 44 | * @method type 45 | * @return {String} type 46 | */ 47 | type () { 48 | return this._type 49 | } 50 | } 51 | 52 | module.exports = Reference 53 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http') 4 | const { Tags, FORMAT_HTTP_HEADERS } = require('opentracing') 5 | const MetricsTracer = require('../src') 6 | 7 | const prometheusReporter = new MetricsTracer.PrometheusReporter() 8 | const metricsTracer = new MetricsTracer('my-server', [prometheusReporter]) 9 | const PORT = process.env.PORT || 3000 10 | 11 | const server = http.createServer((req, res) => { 12 | // Instrumentation 13 | const requestSpan = metricsTracer.startSpan('http_request', { 14 | childOf: metricsTracer.extract(FORMAT_HTTP_HEADERS, req.headers) 15 | }) 16 | const headers = {} 17 | 18 | metricsTracer.inject(requestSpan, FORMAT_HTTP_HEADERS, headers) 19 | 20 | requestSpan.setTag(Tags.HTTP_URL, req.url) 21 | requestSpan.setTag(Tags.HTTP_METHOD, req.method || 'GET') 22 | requestSpan.setTag(Tags.HTTP_STATUS_CODE, 200) 23 | requestSpan.setTag(Tags.SPAN_KIND_RPC_CLIENT, true) 24 | 25 | // Dummy router: GET /metrics 26 | if (req.url === '/metrics') { 27 | requestSpan.finish() 28 | 29 | res.writeHead(200, { 30 | 'Content-Type': MetricsTracer.PrometheusReporter.Prometheus.register.contentType 31 | }) 32 | res.end(prometheusReporter.metrics()) 33 | return 34 | } 35 | 36 | // My child operation like DB access 37 | const childOperationSpan = metricsTracer.startSpan('my_operation', { 38 | childOf: requestSpan 39 | }) 40 | 41 | setTimeout(() => { 42 | childOperationSpan.finish() 43 | 44 | res.writeHead(200, headers) 45 | res.end('Ok') 46 | 47 | requestSpan.finish() 48 | }, 30) 49 | }) 50 | 51 | server.listen(PORT, (err) => { 52 | // eslint-disable-next-line 53 | console.log(err || `Server is listening on ${PORT}`) 54 | }) 55 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## 2.1.0 (2017-09-29) 3 | 4 | 5 | #### Features 6 | 7 | * **tracer:** add ignoreTags option ([acd7c58a](git+https://github.com/RisingStack/opentracing-metrics-tracer.git/commit/acd7c58a)) 8 | 9 | 10 | 11 | ### 2.0.1 (2017-09-04) 12 | 13 | 14 | #### Bug Fixes 15 | 16 | * **reporter:** report empty parent service key as "unknown" ([22a89a58](git+https://github.com/RisingStack/opentracing-metrics-tracer.git/commit/22a89a58)) 17 | 18 | 19 | 20 | ## 2.0.0 (2017-09-04) 21 | 22 | 23 | #### Bug Fixes 24 | 25 | * **reporter:** rename http_request_duration_seconds_bucket to http_request_handler_duration_sec ([b07b5212](git+https://github.com/RisingStack/opentracing-metrics-tracer.git/commit/b07b5212)) 26 | 27 | 28 | #### Breaking Changes 29 | 30 | * metrics renamed http_request_duration_seconds_bucket to http_request_handler_duration_seconds_bucket 31 | 32 | ([b07b5212](git+https://github.com/RisingStack/opentracing-metrics-tracer.git/commit/b07b5212)) 33 | 34 | 35 | 36 | ## 1.3.0 (2017-09-04) 37 | 38 | 39 | #### Features 40 | 41 | * **example:** add child operation ([7a260dfa](git+https://github.com/RisingStack/opentracing-metrics-tracer.git/commit/7a260dfa)) 42 | * **reporter:** report parent_service with http_request_duration_seconds ([a06e2bc0](git+https://github.com/RisingStack/opentracing-metrics-tracer.git/commit/a06e2bc0)) 43 | 44 | 45 | 46 | ## 1.2.0 (2017-09-03) 47 | 48 | 49 | 50 | ## 1.1.0 (2017-09-03) 51 | 52 | 53 | #### Features 54 | 55 | * **tracer:** public getters for Span and SpanContext ([a5b54c15](git+https://github.com/RisingStack/opentracing-metrics-tracer.git/commit/a5b54c15)) 56 | 57 | 58 | 59 | ## 1.0.0 (2017-09-02) 60 | 61 | 62 | #### Features 63 | 64 | * **reporter:** add Prometheus reporter ([74c2b189](git+https://github.com/RisingStack/opentracing-metrics-tracer.git/commit/74c2b189)) 65 | * **tracer:** add Reference, Span, SpanContext and Tracer models ([e87df8e5](git+https://github.com/RisingStack/opentracing-metrics-tracer.git/commit/e87df8e5)) 66 | 67 | -------------------------------------------------------------------------------- /src/tracer/SpanContext.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('assert') 4 | const uuidV1 = require('uuid/v1') 5 | const uuidV4 = require('uuid/v4') 6 | 7 | /** 8 | * @class SpanContext 9 | */ 10 | class SpanContext { 11 | /** 12 | * @constructor 13 | * @param {String} serviceKey 14 | * @param {String} [parentServiceKey] 15 | * @param {String} [traceId] 16 | * @param {String} [spanId] 17 | * @param {String} [parentSpanId] 18 | * @returns {SpanContext} 19 | */ 20 | constructor ( 21 | serviceKey, 22 | parentServiceKey, 23 | traceId = `${uuidV1()}-${uuidV4()}`, 24 | spanId = uuidV4(), 25 | parentSpanId 26 | ) { 27 | assert(typeof serviceKey === 'string', 'serviceKey is required') 28 | 29 | this._serviceKey = serviceKey 30 | this._parentServiceKey = parentServiceKey 31 | this._traceId = traceId 32 | this._spanId = spanId 33 | this._parentSpanId = parentSpanId 34 | this._baggage = {} 35 | } 36 | 37 | /** 38 | * Returns the value for a baggage item given its key 39 | * @method getBaggageItem 40 | * @param {String} key - The key for the given trace attribute 41 | * @returns {String|undefined} value - String value for the given key 42 | * or undefined if the key does not correspond to a set trace attribute 43 | */ 44 | getBaggageItem (key) { 45 | assert(typeof key === 'string', 'key is required') 46 | 47 | return this._baggage[key] 48 | } 49 | 50 | /** 51 | * Sets a key:value pair on this Span that also propagates to future children of the associated Span 52 | * @method setBaggageItem 53 | * @param {String} key 54 | * @param {String} value 55 | * @returns {Span} 56 | */ 57 | setBaggageItem (key, value) { 58 | assert(typeof key === 'string', 'key is required') 59 | assert(typeof value === 'string', 'value is required') 60 | 61 | this._baggage[key] = value 62 | return this 63 | } 64 | 65 | /** 66 | * Returns the parentServiceKey 67 | * @method parentServiceKey 68 | * @returns {String|Undefined} 69 | */ 70 | parentServiceKey () { 71 | return this._parentServiceKey 72 | } 73 | } 74 | 75 | module.exports = SpanContext 76 | -------------------------------------------------------------------------------- /src/tracer/SpanContext.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { expect } = require('chai') 4 | const SpanContext = require('./SpanContext') 5 | 6 | describe('tracer/SpanContext', () => { 7 | describe('#constructor', () => { 8 | it('should create spanContext with parameters', () => { 9 | const spanContext = new SpanContext('service-2', 'service-1', 'trace-id', 'span-id', 'parent-span-id') 10 | 11 | expect(spanContext._serviceKey).to.be.equal('service-2') 12 | expect(spanContext._parentServiceKey).to.be.equal('service-1') 13 | expect(spanContext._traceId).to.be.equal('trace-id') 14 | expect(spanContext._spanId).to.be.equal('span-id') 15 | expect(spanContext._parentSpanId).to.be.equal('parent-span-id') 16 | }) 17 | 18 | it('should generate traceId and spanId', () => { 19 | const spanContext = new SpanContext('service-2', 'service-1') 20 | 21 | expect(spanContext._traceId.length).to.be.equal(73) 22 | expect(spanContext._spanId.length).to.be.equal(36) 23 | }) 24 | }) 25 | 26 | describe('#setBaggageItem', () => { 27 | it('should set baggage item', () => { 28 | const spanContext = new SpanContext('service-1') 29 | spanContext.setBaggageItem('key1', 'value1') 30 | spanContext.setBaggageItem('key2', 'value2') 31 | 32 | expect(spanContext._baggage).to.be.eql({ 33 | key1: 'value1', 34 | key2: 'value2' 35 | }) 36 | }) 37 | }) 38 | 39 | describe('#getBaggageItem', () => { 40 | it('should get baggage item', () => { 41 | const spanContext = new SpanContext('service-1') 42 | spanContext.setBaggageItem('key1', 'value1') 43 | spanContext.setBaggageItem('key2', 'value2') 44 | 45 | expect(spanContext.getBaggageItem('key1')).to.be.equal('value1') 46 | expect(spanContext.getBaggageItem('key2')).to.be.equal('value2') 47 | }) 48 | }) 49 | 50 | describe('#parentServiceKey', () => { 51 | it('should return with the parentServiceKey', () => { 52 | const spanContext = new SpanContext('service-2', 'service-1', 'trace-id', 'span-id', 'parent-span-id') 53 | expect(spanContext.parentServiceKey()).to.be.equal('service-1') 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /src/tracer/Reference.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { expect } = require('chai') 4 | const { REFERENCE_CHILD_OF, REFERENCE_FOLLOWS_FROM } = require('opentracing') 5 | const Reference = require('./Reference') 6 | const Span = require('./Span') 7 | const SpanContext = require('./SpanContext') 8 | const Tracer = require('./Tracer') 9 | 10 | describe('tracer/Reference', () => { 11 | describe('#constructor', () => { 12 | it('should create Reference with SpanContext', () => { 13 | const spanContext = new SpanContext('service-1') 14 | const reference = new Reference(REFERENCE_CHILD_OF, spanContext) 15 | 16 | expect(reference._referencedContext).to.be.eql(spanContext) 17 | }) 18 | 19 | it('should create Reference with Span', () => { 20 | const tracer = new Tracer('service-1') 21 | const spanContext = new SpanContext('service-1') 22 | const span = new Span(tracer, 'operation', spanContext) 23 | const reference = new Reference(REFERENCE_CHILD_OF, span) 24 | 25 | expect(reference._referencedContext).to.be.eql(spanContext) 26 | }) 27 | 28 | it('should create Reference with type REFERENCE_CHILD_OF', () => { 29 | const spanContext = new SpanContext('service-1') 30 | const reference = new Reference(REFERENCE_CHILD_OF, spanContext) 31 | 32 | expect(reference._type).to.be.equal(REFERENCE_CHILD_OF) 33 | }) 34 | 35 | it('should create Reference with type REFERENCE_FOLLOWS_FROM', () => { 36 | const spanContext = new SpanContext('service-1') 37 | const reference = new Reference(REFERENCE_FOLLOWS_FROM, spanContext) 38 | 39 | expect(reference._type).to.be.equal(REFERENCE_FOLLOWS_FROM) 40 | }) 41 | 42 | it('should reject with invalid type', () => { 43 | const spanContext = new SpanContext('service-1') 44 | 45 | expect(() => { 46 | // eslint-disable-next-line 47 | new Reference('invalid', spanContext) 48 | }).to.throw('Invalid type') 49 | }) 50 | }) 51 | 52 | describe('#referencedContext', () => { 53 | it('should get referencedContext', () => { 54 | const spanContext = new SpanContext('service-1') 55 | const reference = new Reference(REFERENCE_CHILD_OF, spanContext) 56 | 57 | expect(reference.referencedContext()).to.be.equal(spanContext) 58 | }) 59 | }) 60 | 61 | describe('#type', () => { 62 | it('should get type', () => { 63 | const spanContext = new SpanContext('service-1') 64 | const reference = new Reference(REFERENCE_CHILD_OF, spanContext) 65 | 66 | expect(reference.type()).to.be.equal(REFERENCE_CHILD_OF) 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /src/reporters/PrometheusReporter.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('assert') 4 | const Prometheus = require('prom-client') 5 | const { Tags } = require('opentracing') 6 | const Span = require('../tracer/Span') 7 | 8 | const DURATION_HISTOGRAM_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10] 9 | const METRICS_NAME_OPERATION_DURATION_SECONDS = 'operation_duration_seconds' 10 | const METRICS_NAME_HTTP_REQUEST_HANDLER_DURATION_SECONDS = 'http_request_handler_duration_seconds' 11 | const LABEL_PARENT_SERVICE_UNKNOWN = 'unknown' 12 | 13 | /** 14 | * Observe span events and expose them in Prometheus metrics format 15 | * @class PrometheusReporter 16 | */ 17 | class PrometheusReporter { 18 | /** 19 | * @static getParentServiceKey 20 | * @param {Span} span 21 | * @return {String} parentService 22 | */ 23 | static getParentService (span) { 24 | const spanContext = span.context() 25 | const parentService = spanContext.parentServiceKey() || LABEL_PARENT_SERVICE_UNKNOWN 26 | 27 | return parentService 28 | } 29 | 30 | /** 31 | * @constructor 32 | * @param {Object} [options={}] 33 | * @param {Object} [options.ignoreTags={}] 34 | * @returns {PrometheusReporter} 35 | */ 36 | constructor ({ ignoreTags = {} } = {}) { 37 | this._registry = new Prometheus.Registry() 38 | this._options = { 39 | ignoreTags 40 | } 41 | 42 | // Initialize metrics 43 | this._metricsOperationDurationSeconds() 44 | } 45 | 46 | /** 47 | * Returns with the reporter's metrics in Prometheus format 48 | * @method metrics 49 | * @returns {Object} metrics 50 | */ 51 | metrics () { 52 | return this._registry.metrics() 53 | } 54 | 55 | /** 56 | * Called by Tracer when a span is finished 57 | * @method reportFinish 58 | * @param {Span} span 59 | */ 60 | reportFinish (span) { 61 | assert(span instanceof Span, 'span is required') 62 | 63 | // Ignore by tag value 64 | const isIgnored = Object.entries(this._options.ignoreTags).some(([tagKey, regexp]) => { 65 | const tagValue = span.getTag(tagKey) 66 | return tagValue && tagValue.match(regexp) 67 | }) 68 | 69 | if (isIgnored) { 70 | return 71 | } 72 | 73 | // Operation metrics 74 | this._reportOperationFinish(span) 75 | 76 | // HTTP Request 77 | if (span.getTag(Tags.SPAN_KIND) === Tags.SPAN_KIND_RPC_SERVER && 78 | (span.getTag(Tags.HTTP_URL) || span.getTag(Tags.HTTP_METHOD) || span.getTag(Tags.HTTP_STATUS_CODE))) { 79 | this._reportHttpRequestFinish(span) 80 | } 81 | } 82 | 83 | /** 84 | * Observe operation metrics 85 | * @method _reportOperationFinish 86 | * @private 87 | * @param {Span} span 88 | */ 89 | _reportOperationFinish (span) { 90 | assert(span instanceof Span, 'span is required') 91 | 92 | this._metricsOperationDurationSeconds() 93 | .labels(PrometheusReporter.getParentService(span), span.operationName()) 94 | .observe(span.duration() / 1000) 95 | } 96 | 97 | /** 98 | * Observe HTTP request metrics 99 | * @method _reportHttpRequestFinish 100 | * @private 101 | * @param {Span} span 102 | */ 103 | _reportHttpRequestFinish (span) { 104 | assert(span instanceof Span, 'span is required') 105 | 106 | this._metricshttpRequestDurationSeconds() 107 | .labels( 108 | PrometheusReporter.getParentService(span), 109 | span.getTag(Tags.HTTP_METHOD), 110 | span.getTag(Tags.HTTP_STATUS_CODE) 111 | ) 112 | .observe(span.duration() / 1000) 113 | } 114 | 115 | /** 116 | * Singleton to get operation duration metrics 117 | * @method _metricsOperationDurationSeconds 118 | * @private 119 | * @return {Prometheus.Histogram} operationDurationSeconds 120 | */ 121 | _metricsOperationDurationSeconds () { 122 | let operationDurationSeconds = this._registry.getSingleMetric(METRICS_NAME_OPERATION_DURATION_SECONDS) 123 | 124 | if (!operationDurationSeconds) { 125 | operationDurationSeconds = new Prometheus.Histogram({ 126 | name: METRICS_NAME_OPERATION_DURATION_SECONDS, 127 | help: 'Duration of operations in second', 128 | labelNames: ['parent_service', 'name'], 129 | buckets: DURATION_HISTOGRAM_BUCKETS, 130 | registers: [this._registry] 131 | }) 132 | } 133 | 134 | return operationDurationSeconds 135 | } 136 | 137 | /** 138 | * Singleton to get HTTP request duration metrics 139 | * @method _metricshttpRequestDurationSeconds 140 | * @private 141 | * @return {Prometheus.Histogram} httpRequestDurationSeconds 142 | */ 143 | _metricshttpRequestDurationSeconds () { 144 | let httpRequestDurationSeconds = this._registry.getSingleMetric(METRICS_NAME_HTTP_REQUEST_HANDLER_DURATION_SECONDS) 145 | 146 | if (!httpRequestDurationSeconds) { 147 | httpRequestDurationSeconds = new Prometheus.Histogram({ 148 | name: METRICS_NAME_HTTP_REQUEST_HANDLER_DURATION_SECONDS, 149 | help: 'Duration of HTTP requests in second', 150 | labelNames: ['parent_service', 'method', 'code'], 151 | buckets: DURATION_HISTOGRAM_BUCKETS, 152 | registers: [this._registry] 153 | }) 154 | } 155 | 156 | return httpRequestDurationSeconds 157 | } 158 | } 159 | 160 | PrometheusReporter.Prometheus = Prometheus 161 | PrometheusReporter.LABEL_PARENT_SERVICE_UNKNOWN = LABEL_PARENT_SERVICE_UNKNOWN 162 | 163 | module.exports = PrometheusReporter 164 | -------------------------------------------------------------------------------- /src/tracer/Span.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('assert') 4 | const SpanContext = require('./SpanContext') 5 | 6 | /** 7 | * Follows the original opentracing API 8 | * @class Span 9 | */ 10 | class Span { 11 | /** 12 | * @constructor 13 | * @param {Tracer} tracer 14 | * @param {String} operationName 15 | * @param {SpanContext} spanContext 16 | * @param {Object} tags 17 | * @param {Number} startTime 18 | * @param {Array} references - Array of Reference 19 | * @returns {Span} 20 | */ 21 | constructor ( 22 | tracer, 23 | operationName, 24 | spanContext, 25 | tags = {}, 26 | startTime = Date.now(), 27 | references = [] 28 | ) { 29 | assert(tracer, 'tracer is required') 30 | assert(spanContext instanceof SpanContext, 'spanContext is required') 31 | assert(typeof operationName === 'string', 'operationName is required') 32 | 33 | this._tracer = tracer 34 | this._operationName = operationName 35 | this._spanContext = spanContext 36 | this._startTime = startTime 37 | this._references = references 38 | this._logs = [] 39 | this._tags = tags 40 | this._duration = undefined 41 | } 42 | 43 | /** 44 | * Adds the given key value pairs to the set of span tags 45 | * @method addTags 46 | * @param {Object} keyValueMap - [key: string]: any 47 | * @returns {Span} 48 | */ 49 | addTags (keyValueMap) { 50 | assert(typeof keyValueMap === 'object', 'keyValueMap is required') 51 | 52 | this._tags = Object.assign(this._tags, keyValueMap) 53 | 54 | return this 55 | } 56 | 57 | /** 58 | * Returns the SpanContext object associated with this Span 59 | * @method context 60 | * @returns {SpanContext} 61 | */ 62 | context () { 63 | return this._spanContext 64 | } 65 | 66 | /** 67 | * Sets the end timestamp and finalizes Span state 68 | * @method finishTime 69 | * @param {Number} [finishTime] - Optional finish time in milliseconds as a Unix timestamp 70 | */ 71 | finish (finishTime) { 72 | assert(finishTime === undefined || typeof finishTime === 'number', 'finishTime is required') 73 | 74 | this._duration = (finishTime || Date.now()) - this._startTime 75 | } 76 | 77 | /** 78 | * Returns the value for a baggage item given its key 79 | * @method getBaggageItem 80 | * @param {String} key - The key for the given trace attribute 81 | * @returns {String|undefined} value - String value for the given key 82 | * or undefined if the key does not correspond to a set trace attribute 83 | */ 84 | getBaggageItem (key) { 85 | assert(typeof key === 'string', 'key is required') 86 | 87 | return this._spanContext.getBaggageItem(key) 88 | } 89 | 90 | /** 91 | * Add a log record to this Span, optionally at a user-provided timestamp 92 | * @method log 93 | * @param {Object} keyValuePairs - An object mapping string keys to arbitrary value types 94 | * @param {Number} [timestamp] - An optional parameter specifying the timestamp in milliseconds 95 | * since the Unix epoch 96 | * @returns {Span} 97 | */ 98 | log (keyValuePairs, timestamp) { 99 | assert(typeof keyValuePairs === 'object', 'keyValuePairs is required') 100 | assert(timestamp === undefined || typeof timestamp === 'number', 'timestamp is required') 101 | 102 | this._logs.push({ 103 | time: timestamp || Date.now(), 104 | data: keyValuePairs 105 | }) 106 | 107 | return this 108 | } 109 | 110 | /** 111 | * @method logEvent 112 | * @deprecated 113 | */ 114 | // eslint-disable-next-line 115 | logEvent () {} 116 | 117 | /** 118 | * Sets a key:value pair on this Span that also propagates to future children of the associated Span 119 | * @method setBaggageItem 120 | * @param {String} key 121 | * @param {String} value 122 | * @returns {Span} 123 | */ 124 | setBaggageItem (key, value) { 125 | assert(typeof key === 'string', 'key is required') 126 | assert(typeof value === 'string', 'value is required') 127 | 128 | this._spanContext.setBaggageItem(key, value) 129 | return this 130 | } 131 | 132 | /** 133 | * Sets the string name for the logical operation this span represents 134 | * @method setOperationName 135 | * @param {String} operationName 136 | * @returns {Span} 137 | */ 138 | setOperationName (operationName) { 139 | assert(typeof operationName === 'string', 'operationName is required') 140 | 141 | this._operationName = operationName 142 | return this 143 | } 144 | 145 | /** 146 | * Adds a single tag to the span. See addTags() for details 147 | * @method setTag 148 | * @param {String} key 149 | * @param {*} value 150 | * @returns {Span} 151 | */ 152 | setTag (key, value) { 153 | assert(typeof key === 'string', 'key is required') 154 | assert(value !== undefined, 'value is required') 155 | 156 | this._tags[key] = value 157 | return this 158 | } 159 | 160 | /** 161 | * Returns the Tracer object used to create this Span 162 | * @method tracer 163 | * @returns {Tracer} 164 | */ 165 | tracer () { 166 | return this._tracer 167 | } 168 | } 169 | 170 | /** 171 | * Extends the original opentracing API with getters for reporting 172 | * @class MetricsSpan 173 | * @extends Span 174 | */ 175 | class MetricsSpan extends Span { 176 | /** 177 | * Get operation name 178 | * @method operationName 179 | * @returns {String} operationName 180 | */ 181 | operationName () { 182 | return this._operationName 183 | } 184 | 185 | /** 186 | * Get duration 187 | * @method duration 188 | * @returns {Number|Undefined} duration 189 | */ 190 | duration () { 191 | return this._duration 192 | } 193 | 194 | /** 195 | * Get a single tag 196 | * @method getTag 197 | * @param {String} key 198 | * @returns {String} value 199 | */ 200 | getTag (key) { 201 | assert(typeof key === 'string', 'key is required') 202 | 203 | return this._tags[key] 204 | } 205 | 206 | /** 207 | * Sets the end timestamp and finalizes Span state 208 | * @method finishTime 209 | * @param {Number} [finishTime] - Optional finish time in milliseconds as a Unix timestamp 210 | */ 211 | finish (finishTime) { 212 | super.finish(finishTime) 213 | 214 | this._tracer.reportFinish(this) 215 | } 216 | } 217 | 218 | module.exports = MetricsSpan 219 | -------------------------------------------------------------------------------- /src/tracer/Tracer.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { expect } = require('chai') 4 | const { FORMAT_HTTP_HEADERS, FORMAT_BINARY } = require('opentracing') 5 | const Span = require('./Span') 6 | const SpanContext = require('./SpanContext') 7 | const Tracer = require('./Tracer') 8 | 9 | describe('tracer/Tracer', () => { 10 | describe('#constructor', () => { 11 | it('should create a tracer', () => { 12 | const tracer = new Tracer('service-key', ['reporter']) 13 | 14 | expect(tracer._serviceKey).to.be.equal('service-key') 15 | expect(tracer._reporters).to.be.eql(['reporter']) 16 | }) 17 | }) 18 | 19 | describe('#startSpan', () => { 20 | it('should start a span', () => { 21 | const tracer = new Tracer('service-key') 22 | const span = tracer.startSpan('my-operation') 23 | 24 | expect(span).to.be.instanceof(Span) 25 | }) 26 | 27 | it('should start a span with options.childOf', () => { 28 | const tracer1 = new Tracer('service-1') 29 | const parentSpanContext = new SpanContext('service-1') 30 | const parentSpan = new Span(tracer1, 'operation', parentSpanContext) 31 | 32 | const tracer2 = new Tracer('service-2') 33 | const span = tracer2.startSpan('my-operation', { 34 | childOf: parentSpan 35 | }) 36 | const spanContext = span.context() 37 | 38 | expect(spanContext._serviceKey).to.be.equal('service-2') 39 | expect(spanContext._parentServiceKey).to.be.equal(parentSpanContext._serviceKey) 40 | expect(spanContext._traceId).to.be.equal(parentSpanContext._traceId) 41 | expect(spanContext._spanId).to.be.not.equal(parentSpanContext._spanId) 42 | expect(spanContext._parentSpanId).to.be.equal(parentSpanContext._spanId) 43 | }) 44 | 45 | it('should start a span with options.references and options.childOf', () => { 46 | const tracer1 = new Tracer('service-1') 47 | const parentSpanContext = new SpanContext('service-1') 48 | const parentSpan = new Span(tracer1, 'operation', parentSpanContext) 49 | 50 | const tracer2 = new Tracer('service-2') 51 | const span = tracer2.startSpan('my-operation', { 52 | childOf: parentSpan, 53 | references: [] 54 | }) 55 | const spanContext = span.context() 56 | 57 | expect(spanContext._serviceKey).to.be.equal('service-2') 58 | expect(spanContext._parentServiceKey).to.be.equal(parentSpanContext._serviceKey) 59 | expect(spanContext._traceId).to.be.equal(parentSpanContext._traceId) 60 | expect(spanContext._spanId).to.be.not.equal(parentSpanContext._spanId) 61 | expect(spanContext._parentSpanId).to.be.equal(parentSpanContext._spanId) 62 | }) 63 | }) 64 | 65 | describe('#inject', () => { 66 | it('should inject Span', () => { 67 | const tracer = new Tracer('service-key') 68 | const span = tracer.startSpan('my-operation') 69 | const spanContext = span.context() 70 | const carrier = {} 71 | 72 | tracer.inject(span, FORMAT_HTTP_HEADERS, carrier) 73 | 74 | expect(carrier).to.be.eql({ 75 | [Tracer.CARRIER_KEY_SERVICE_KEYS]: spanContext._serviceKey, 76 | [Tracer.CARRIER_KEY_TRACE_ID]: spanContext._traceId, 77 | [Tracer.CARRIER_KEY_SPAN_IDS]: spanContext._spanId 78 | }) 79 | }) 80 | 81 | it('should inject SpanContext', () => { 82 | const tracer = new Tracer('service-key') 83 | const span = tracer.startSpan('my-operation') 84 | const spanContext = span.context() 85 | const carrier = {} 86 | 87 | tracer.inject(spanContext, FORMAT_HTTP_HEADERS, carrier) 88 | 89 | expect(carrier).to.be.eql({ 90 | [Tracer.CARRIER_KEY_SERVICE_KEYS]: spanContext._serviceKey, 91 | [Tracer.CARRIER_KEY_TRACE_ID]: spanContext._traceId, 92 | [Tracer.CARRIER_KEY_SPAN_IDS]: spanContext._spanId 93 | }) 94 | }) 95 | 96 | it('should inject SpanContext with parent', () => { 97 | const tracer1 = new Tracer('service-1') 98 | const parentSpanContext = new SpanContext('service-1') 99 | const parentSpan = new Span(tracer1, 'operation', parentSpanContext) 100 | 101 | const tracer2 = new Tracer('service-2') 102 | const span = tracer2.startSpan('my-operation', { 103 | childOf: parentSpan 104 | }) 105 | const spanContext = span.context() 106 | const carrier = {} 107 | 108 | tracer2.inject(spanContext, FORMAT_HTTP_HEADERS, carrier) 109 | 110 | expect(carrier).to.be.eql({ 111 | [Tracer.CARRIER_KEY_SERVICE_KEYS]: `${spanContext._serviceKey}:${spanContext._parentServiceKey}`, 112 | [Tracer.CARRIER_KEY_TRACE_ID]: spanContext._traceId, 113 | [Tracer.CARRIER_KEY_SPAN_IDS]: `${spanContext._spanId}:${spanContext._parentSpanId}` 114 | }) 115 | }) 116 | 117 | it('should not inject with unsupported format', () => { 118 | const tracer = new Tracer('service-key') 119 | const span = tracer.startSpan('my-operation') 120 | const spanContext = span.context() 121 | const carrier = {} 122 | 123 | tracer.inject(spanContext, FORMAT_BINARY, carrier) 124 | 125 | expect(carrier).to.be.eql({}) 126 | }) 127 | }) 128 | 129 | describe('#extract', () => { 130 | it('should extract SpanContext', () => { 131 | const tracer = new Tracer('service-key') 132 | const span = tracer.startSpan('my-operation') 133 | const spanContext = span.context() 134 | const carrier = {} 135 | 136 | tracer.inject(span, FORMAT_HTTP_HEADERS, carrier) 137 | const spanContextExtracted = tracer.extract(FORMAT_HTTP_HEADERS, carrier) 138 | 139 | expect(spanContextExtracted).to.be.eql(spanContext) 140 | }) 141 | 142 | it('should return null with invalid carrier', () => { 143 | const tracer = new Tracer('service-key') 144 | const spanContextExtracted = tracer.extract(FORMAT_HTTP_HEADERS, {}) 145 | 146 | expect(spanContextExtracted).to.be.eql(null) 147 | }) 148 | 149 | it('should return null with unsupported format', () => { 150 | const tracer = new Tracer('service-key') 151 | const spanContextExtracted = tracer.extract(FORMAT_BINARY, {}) 152 | 153 | expect(spanContextExtracted).to.be.eql(null) 154 | }) 155 | }) 156 | 157 | describe('#reportFinish', () => { 158 | it('should call reporters', function () { 159 | const reporter1 = { 160 | reportFinish: this.sandbox.spy() 161 | } 162 | const reporter2 = { 163 | reportFinish: this.sandbox.spy() 164 | } 165 | const tracer = new Tracer('service-key', [reporter1, reporter2]) 166 | const span = tracer.startSpan('my-operation') 167 | 168 | tracer.reportFinish(span) 169 | 170 | expect(reporter1.reportFinish).to.be.calledWith(span) 171 | expect(reporter2.reportFinish).to.be.calledWith(span) 172 | }) 173 | }) 174 | }) 175 | -------------------------------------------------------------------------------- /src/tracer/Tracer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('assert') 4 | const { FORMAT_BINARY, FORMAT_TEXT_MAP, FORMAT_HTTP_HEADERS, REFERENCE_CHILD_OF } = require('opentracing') 5 | const Span = require('./Span') 6 | const SpanContext = require('./SpanContext') 7 | const Reference = require('./Reference') 8 | 9 | const CARRIER_KEY_SERVICE_KEYS = 'metrics-tracer-service-key' 10 | const CARRIER_KEY_TRACE_ID = 'metrics-tracer-trace-id' 11 | const CARRIER_KEY_SPAN_IDS = 'metrics-tracer-span-id' 12 | 13 | /** 14 | * Tracer is the entry-point between the instrumentation API and the tracing implementation 15 | * Follows the original opentracing API 16 | * @class Tracer 17 | */ 18 | class Tracer { 19 | /** 20 | * @constructor 21 | * @param {String} serviceKey 22 | * @param {Array} reporters 23 | * @returns {Tracer} 24 | */ 25 | constructor (serviceKey, reporters = []) { 26 | this._serviceKey = serviceKey 27 | this._reporters = reporters 28 | } 29 | 30 | /** 31 | * @method extract 32 | * @param {String} operationName - the name of the operation 33 | * @param {Object} options 34 | * @param {SpanContext} [options.childOf] - a parent SpanContext (or Span, 35 | * for convenience) that the newly-started span will be the child of 36 | * (per REFERENCE_CHILD_OF). If specified, `fields.references` must 37 | * be unspecified. 38 | * @param {array} [options.references] - an array of Reference instances, 39 | * each pointing to a causal parent SpanContext. If specified, 40 | * `fields.childOf` must be unspecified. 41 | * @param {object} [options.tags] - set of key-value pairs which will be set 42 | * as tags on the newly created Span. Ownership of the object is 43 | * passed to the created span for efficiency reasons (the caller 44 | * should not modify this object after calling startSpan). 45 | * @param {number} [options.startTime] - a manually specified start time for 46 | * the created Span object. The time should be specified in 47 | * milliseconds as Unix timestamp. Decimal value are supported 48 | * to represent time values with sub-millisecond accuracy. 49 | * @returns {Span} span - a new Span object 50 | */ 51 | startSpan (operationName, options = {}) { 52 | assert(typeof operationName === 'string', 'operationName is required') 53 | 54 | let spanContext 55 | 56 | // Handle options.childOf 57 | if (options.childOf) { 58 | const childOf = new Reference(REFERENCE_CHILD_OF, options.childOf) 59 | 60 | if (options.references) { 61 | options.references.push(childOf) 62 | } else { 63 | options.references = [childOf] 64 | } 65 | 66 | const parentSpanContext = childOf.referencedContext() 67 | const serviceKey = this._serviceKey 68 | const parentServiceKey = parentSpanContext._serviceKey 69 | const traceId = parentSpanContext._traceId 70 | const spanId = undefined 71 | const parentSpanId = parentSpanContext._spanId 72 | 73 | spanContext = new SpanContext( 74 | serviceKey, 75 | parentServiceKey, 76 | traceId, 77 | spanId, 78 | parentSpanId 79 | ) 80 | } 81 | 82 | spanContext = spanContext || new SpanContext(this._serviceKey) 83 | 84 | return new Span( 85 | this, 86 | operationName, 87 | spanContext, 88 | options.tags, 89 | options.startTime, 90 | options.references 91 | ) 92 | } 93 | 94 | /** 95 | * @method extract 96 | * @param {String} format - the format of the carrier 97 | * @param {*} carrier - the type of the carrier object is determined by the format 98 | * @returns {SpanContext|null} - The extracted SpanContext, or null if no such SpanContext could 99 | * be found in carrier 100 | */ 101 | // eslint-disable-next-line class-methods-use-this 102 | extract (format, carrier) { 103 | assert(format, [FORMAT_BINARY, FORMAT_TEXT_MAP, FORMAT_HTTP_HEADERS].includes(format), 'Invalid type') 104 | assert(typeof carrier === 'object', 'carrier is required') 105 | 106 | if (format === FORMAT_BINARY) { 107 | // TODO: log 108 | } else { 109 | const tmpServiceKeys = (carrier[Tracer.CARRIER_KEY_SERVICE_KEYS] || '').split(':') 110 | const tmpSpanKeys = (carrier[Tracer.CARRIER_KEY_SPAN_IDS] || '').split(':') 111 | 112 | const serviceKey = tmpServiceKeys.shift() 113 | const parentServiceKey = tmpServiceKeys.shift() || undefined 114 | const traceId = carrier[Tracer.CARRIER_KEY_TRACE_ID] 115 | const spanId = tmpSpanKeys.shift() 116 | const parentSpanId = tmpSpanKeys.shift() || undefined 117 | 118 | if (!serviceKey || !traceId || !spanId) { 119 | return null 120 | } 121 | 122 | return new SpanContext( 123 | serviceKey, 124 | parentServiceKey, 125 | traceId, 126 | spanId, 127 | parentSpanId 128 | ) 129 | } 130 | 131 | return null 132 | } 133 | 134 | /** 135 | * @method inject 136 | * @param {SpanContext|Span} spanContext - he SpanContext to inject into the carrier object. 137 | * As a convenience, a Span instance may be passed in instead 138 | * (in which case its .context() is used for the inject()) 139 | * @param {String} format - the format of the carrier 140 | * @param {*} carrier - the type of the carrier object is determined by the format 141 | */ 142 | // eslint-disable-next-line class-methods-use-this 143 | inject (spanContext, format, carrier) { 144 | assert(spanContext, 'spanContext is required') 145 | assert(format, [FORMAT_BINARY, FORMAT_TEXT_MAP, FORMAT_HTTP_HEADERS].includes(format), 'Invalid type') 146 | assert(typeof carrier === 'object', 'carrier is required') 147 | 148 | const injectedContext = spanContext instanceof Span ? 149 | spanContext.context() : spanContext 150 | 151 | if (format === FORMAT_BINARY) { 152 | // TODO: log not implemented 153 | } else { 154 | let serviceKeysStr = injectedContext._serviceKey 155 | if (injectedContext.parentServiceKey()) { 156 | serviceKeysStr += `:${injectedContext.parentServiceKey()}` 157 | } 158 | 159 | let spanIdsStr = injectedContext._spanId 160 | if (injectedContext._parentSpanId) { 161 | spanIdsStr += `:${injectedContext._parentSpanId}` 162 | } 163 | 164 | carrier[Tracer.CARRIER_KEY_SERVICE_KEYS] = serviceKeysStr 165 | carrier[Tracer.CARRIER_KEY_TRACE_ID] = injectedContext._traceId 166 | carrier[Tracer.CARRIER_KEY_SPAN_IDS] = spanIdsStr 167 | } 168 | } 169 | } 170 | 171 | Tracer.CARRIER_KEY_SERVICE_KEYS = CARRIER_KEY_SERVICE_KEYS 172 | Tracer.CARRIER_KEY_TRACE_ID = CARRIER_KEY_TRACE_ID 173 | Tracer.CARRIER_KEY_SPAN_IDS = CARRIER_KEY_SPAN_IDS 174 | 175 | /** 176 | * Extends the original opentracing API 177 | * @class MetricsTracer 178 | * @extends Tracer 179 | */ 180 | class MetricsTracer extends Tracer { 181 | /** 182 | * @method reportFinish 183 | * @param {Span} span 184 | */ 185 | reportFinish (span) { 186 | this._reporters.forEach((reporter) => reporter.reportFinish(span)) 187 | } 188 | } 189 | 190 | module.exports = MetricsTracer 191 | -------------------------------------------------------------------------------- /src/tracer/Span.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const sinon = require('sinon') 4 | const { expect } = require('chai') 5 | const { Tags } = require('opentracing') 6 | const Span = require('./Span') 7 | const SpanContext = require('./SpanContext') 8 | const Tracer = require('./Tracer') 9 | 10 | describe('tracer/Span', () => { 11 | let clock 12 | 13 | beforeEach(() => { 14 | clock = sinon.useFakeTimers() 15 | }) 16 | 17 | afterEach(() => { 18 | clock.restore() 19 | }) 20 | 21 | describe('#constructor', () => { 22 | it('should create a Span', () => { 23 | const tracer = new Tracer('service-1') 24 | const spanContext = new SpanContext('service-1') 25 | const span = new Span(tracer, 'operation', spanContext) 26 | 27 | expect(span._startTime).to.be.lte(0) 28 | }) 29 | }) 30 | 31 | describe('#addTags', () => { 32 | it('should add a tag', () => { 33 | const tracer = new Tracer('service-1') 34 | const spanContext = new SpanContext('service-1') 35 | const span = new Span(tracer, 'operation', spanContext) 36 | const tags = { 37 | [Tags.HTTP_METHOD]: 'GET', 38 | [Tags.SPAN_KIND_RPC_CLIENT]: true 39 | } 40 | 41 | span.addTags(tags) 42 | 43 | expect(span._tags).to.be.eql(tags) 44 | }) 45 | }) 46 | 47 | describe('#context', () => { 48 | it('should get context', () => { 49 | const tracer = new Tracer('service-1') 50 | const spanContext = new SpanContext('service-1') 51 | const span = new Span(tracer, 'operation', spanContext) 52 | 53 | expect(span.context()).to.be.eql(spanContext) 54 | }) 55 | }) 56 | 57 | describe('#finish', () => { 58 | it('should calculate duration in milliseconds', function () { 59 | const tracer = new Tracer('service-1') 60 | const spanContext = new SpanContext('service-1') 61 | const span = new Span(tracer, 'operation', spanContext) 62 | 63 | this.sandbox.spy(tracer, 'reportFinish') 64 | 65 | clock.tick(150) 66 | 67 | span.finish() 68 | 69 | expect(span._duration).to.be.equal(150) 70 | expect(tracer.reportFinish).to.be.calledWith(span) 71 | }) 72 | 73 | it('should use passed finishTime', () => { 74 | const tracer = new Tracer('service-1') 75 | const spanContext = new SpanContext('service-1') 76 | const span = new Span(tracer, 'operation', spanContext) 77 | 78 | clock.tick(150) 79 | 80 | span.finish(110) 81 | 82 | expect(span._duration).to.be.equal(110) 83 | }) 84 | }) 85 | 86 | describe('#getBaggageItem', () => { 87 | it('should get baggage item', () => { 88 | const tracer = new Tracer('service-1') 89 | const spanContext = new SpanContext('service-1') 90 | const span = new Span(tracer, 'operation', spanContext) 91 | 92 | spanContext.setBaggageItem('key1', 'value1') 93 | spanContext.setBaggageItem('key2', 'value2') 94 | 95 | expect(span.getBaggageItem('key1')).to.be.equal('value1') 96 | expect(span.getBaggageItem('key2')).to.be.equal('value2') 97 | }) 98 | }) 99 | 100 | describe('#operationName', () => { 101 | it('should get operation name', () => { 102 | const tracer = new Tracer('service-1') 103 | const spanContext = new SpanContext('service-1') 104 | const span = new Span(tracer, 'operation', spanContext) 105 | 106 | expect(span.operationName()).to.be.equal('operation') 107 | }) 108 | }) 109 | 110 | describe('#duration', () => { 111 | it('should get duration', () => { 112 | const tracer = new Tracer('service-1') 113 | const spanContext = new SpanContext('service-1') 114 | const span = new Span(tracer, 'operation', spanContext) 115 | clock.tick(100) 116 | span.finish() 117 | 118 | expect(span.duration()).to.be.equal(100) 119 | }) 120 | }) 121 | 122 | describe('#getTag', () => { 123 | it('should get a tag value', () => { 124 | const tracer = new Tracer('service-1') 125 | const spanContext = new SpanContext('service-1') 126 | const span = new Span(tracer, 'operation', spanContext) 127 | span.setTag(Tags.HTTP_METHOD, 'GET') 128 | 129 | expect(span.getTag(Tags.HTTP_METHOD)).to.be.equal('GET') 130 | }) 131 | }) 132 | 133 | describe('#log', () => { 134 | it('should log', () => { 135 | const tracer = new Tracer('service-1') 136 | const spanContext = new SpanContext('service-1') 137 | const span = new Span(tracer, 'operation', spanContext) 138 | 139 | clock.tick(10) 140 | span.log({ foo: 'bar' }) 141 | 142 | clock.tick(10) 143 | span.log({ so: 'such' }) 144 | 145 | expect(span._logs).to.be.eql([ 146 | { 147 | time: 10, 148 | data: { foo: 'bar' } 149 | }, 150 | { 151 | time: 20, 152 | data: { so: 'such' } 153 | } 154 | ]) 155 | }) 156 | 157 | it('should log with timestamp', () => { 158 | const tracer = new Tracer('service-1') 159 | const spanContext = new SpanContext('service-1') 160 | const span = new Span(tracer, 'operation', spanContext) 161 | 162 | clock.tick(10) 163 | span.log({ foo: 'bar' }, 5) 164 | 165 | clock.tick(10) 166 | span.log({ so: 'such' }, 15) 167 | 168 | expect(span._logs).to.be.eql([ 169 | { 170 | time: 5, 171 | data: { foo: 'bar' } 172 | }, 173 | { 174 | time: 15, 175 | data: { so: 'such' } 176 | } 177 | ]) 178 | }) 179 | }) 180 | 181 | describe('#logEvent', () => { 182 | it('should do nothing as it\'s deprectaed', () => { 183 | const tracer = new Tracer('service-1') 184 | const spanContext = new SpanContext('service-1') 185 | const span = new Span(tracer, 'operation', spanContext) 186 | 187 | span.logEvent() 188 | }) 189 | }) 190 | 191 | describe('#setBaggageItem', () => { 192 | it('should set baggage item', () => { 193 | const tracer = new Tracer('service-1') 194 | const spanContext = new SpanContext('service-1') 195 | const span = new Span(tracer, 'operation', spanContext) 196 | 197 | span.setBaggageItem('key1', 'value1') 198 | span.setBaggageItem('key2', 'value2') 199 | 200 | expect(span.getBaggageItem('key1')).to.be.equal('value1') 201 | expect(span.getBaggageItem('key2')).to.be.equal('value2') 202 | }) 203 | }) 204 | 205 | describe('#setOperationName', () => { 206 | it('should set baggage item', () => { 207 | const tracer = new Tracer('service-1') 208 | const spanContext = new SpanContext('service-1') 209 | const span = new Span(tracer, 'operation', spanContext) 210 | 211 | expect(span._operationName).to.be.equal('operation') 212 | 213 | span.setOperationName('operation2') 214 | 215 | expect(span._operationName).to.be.equal('operation2') 216 | }) 217 | }) 218 | 219 | describe('#setTag', () => { 220 | it('should set a tag', () => { 221 | const tracer = new Tracer('service-1') 222 | const spanContext = new SpanContext('service-1') 223 | const span = new Span(tracer, 'operation', spanContext) 224 | 225 | span.setTag(Tags.HTTP_METHOD, 'GET') 226 | span.setTag(Tags.SPAN_KIND_RPC_CLIENT, true) 227 | 228 | expect(span._tags).to.be.eql({ 229 | [Tags.HTTP_METHOD]: 'GET', 230 | [Tags.SPAN_KIND_RPC_CLIENT]: true 231 | }) 232 | }) 233 | }) 234 | 235 | describe('#tracer', () => { 236 | it('should get tracer', () => { 237 | const tracer = new Tracer('service-1') 238 | const spanContext = new SpanContext('service-1') 239 | const span = new Span(tracer, 'operation', spanContext) 240 | 241 | expect(span.tracer()).to.be.eql(tracer) 242 | }) 243 | }) 244 | }) 245 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # opentracing-metrics-tracer 2 | 3 | [![Build Status](https://travis-ci.org/RisingStack/opentracing-metrics-tracer.svg?branch=master)](https://travis-ci.org/RisingStack/opentracing-metrics-tracer) 4 | 5 | Exports cross-process metrics via OpenTracing instrumentation to reporters: Prometheus. 6 | It's capable to measure operation characteristics in a distributed system like microservices. 7 | 8 | It also makes possible to reverse engineer the infrastructure topology as we know the initiators 9 | 10 | ## Available Reporters 11 | 12 | - [Prometheus](https://prometheus.io/) via [prom-client](https://github.com/siimon/prom-client) 13 | 14 | ## Getting started 15 | 16 | ```js 17 | const MetricsTracer = require('@risingstack/opentracing-metrics-tracer') 18 | const prometheusReporter = new MetricsTracer.PrometheusReporter() 19 | const metricsTracer = new MetricsTracer('my-service', [prometheusReporter]) 20 | 21 | // Instrument 22 | const span = metricsTracer.startSpan('my-operation') 23 | span.finish() 24 | ... 25 | 26 | app.get('/metrics', (req, res) => { 27 | res.set('Content-Type', MetricsTracer.PrometheusReporter.Prometheus.register.contentType) 28 | res.end(prometheusReporter.metrics()) 29 | }) 30 | ``` 31 | 32 | ### With auto instrumentation and multiple tracers 33 | 34 | Check out: https://github.com/RisingStack/opentracing-auto 35 | 36 | ```js 37 | // Prometheus metrics tracer 38 | const MetricsTracer = require('@risingstack/opentracing-metrics-tracer') 39 | const prometheusReporter = new MetricsTracer.PrometheusReporter() 40 | const metricsTracer = new MetricsTracer('my-service', [prometheusReporter]) 41 | 42 | // Jaeger tracer (classic distributed tracing) 43 | const jaeger = require('jaeger-client') 44 | const UDPSender = require('jaeger-client/dist/src/reporters/udp_sender').default 45 | const sampler = new jaeger.RateLimitingSampler(1) 46 | const reporter = new jaeger.RemoteReporter(new UDPSender()) 47 | const jaegerTracer = new jaeger.Tracer('my-server-pg', reporter, sampler) 48 | 49 | // Auto instrumentation 50 | const Instrument = require('@risingstack/opentracing-auto') 51 | const instrument = new Instrument({ 52 | tracers: [metricsTracer, jaegerTracer] 53 | }) 54 | 55 | // Rest of your code 56 | const express = require('express') 57 | const app = express() 58 | 59 | app.get('/metrics', (req, res) => { 60 | res.set('Content-Type', MetricsTracer.PrometheusReporter.Prometheus.register.contentType) 61 | res.end(prometheusReporter.metrics()) 62 | }) 63 | ``` 64 | 65 | ### Example 66 | 67 | See [example server](/example/server.js). 68 | 69 | ```sh 70 | node example/server 71 | curl http://localhost:3000 72 | curl http://localhost:3000/metrics 73 | ``` 74 | 75 | ## API 76 | 77 | `const Tracer = require('@risingstack/opentracing-metrics-tracer')` 78 | 79 | ### new Tracer(serviceKey, [reporter1, reporter2, ...]) 80 | 81 | - **serviceKey** *String*, *required*, unique key that identifies a specific type of service *(for example: my-frontend-api)* 82 | - **reporters** *Array of reporters*, *optional*, *default:* [] 83 | 84 | [OpenTracing](https://github.com/opentracing/opentracing-javascript) compatible tracer, for the complete API check out the official [documentation](https://opentracing-javascript.surge.sh/). 85 | 86 | ### new Tracer.PrometheusReporter([opts]) 87 | 88 | - **opts** *Object*, *optional* 89 | - **opts.ignoreTags** *Object*, *optional* 90 | - Example: `{ ignoreTags: { [Tags.HTTP_URL]: /\/metrics$/ } }` to ignore Prometheus scraper 91 | 92 | Creates a new Prometheus reporter. 93 | 94 | ### Tracer.PrometheusReporter.Prometheus 95 | 96 | Exposed [prom-client](https://github.com/siimon/prom-client). 97 | 98 | ## Reporters 99 | 100 | ### Prometheus Reporter 101 | 102 | Exposes metrics in Prometheus format via [prom-client](https://github.com/siimon/prom-client) 103 | 104 | #### Metrics 105 | 106 | - [operation_duration_seconds](#operation_duration_seconds) 107 | - [http_request_duration_seconds](#http_request_duration_seconds) 108 | 109 | ##### operation_duration_seconds 110 | 111 | Always measured. 112 | Sample output: Two distributed services communicate over the network. 113 | 114 | ``` 115 | # HELP operation_duration_seconds Duration of operations in second 116 | # TYPE operation_duration_seconds histogram 117 | operation_duration_seconds_bucket{le="0.005",parent_service="my-parent-service",name="my-operation" 0 118 | operation_duration_seconds_bucket{le="0.01",parent_service="my-parent-service",name="my-operation" 0 119 | operation_duration_seconds_bucket{le="0.025",parent_service="my-parent-service",name="my-operation" 0 120 | operation_duration_seconds_bucket{le="0.05",parent_service="my-parent-service",name="my-operation" 0 121 | operation_duration_seconds_bucket{le="0.1",parent_service="my-parent-service",name="my-operation" 1 122 | operation_duration_seconds_bucket{le="0.25",parent_service="my-parent-service",name="my-operation" 1 123 | operation_duration_seconds_bucket{le="0.5",parent_service="my-parent-service",name="my-operation" 2 124 | operation_duration_seconds_bucket{le="1",parent_service="my-parent-service",name="my-operation" 2 125 | operation_duration_seconds_bucket{le="2.5",parent_service="my-parent-service",name="my-operation" 2 126 | operation_duration_seconds_bucket{le="5",parent_service="my-parent-service",name="my-operation" 2 127 | operation_duration_seconds_bucket{le="10",parent_service="my-parent-service",name="my-operation" 2 128 | operation_duration_seconds_bucket{le="+Inf",parent_service="my-parent-service",name="my-operation" 2 129 | operation_duration_seconds_sum{parent_service="my-parent-service",name="my-operation" 0.4 130 | operation_duration_seconds_count{parent_service="my-parent-service",name="my-operation" 2 131 | ``` 132 | 133 | ##### http_request_duration_seconds 134 | 135 | Measured only when the span is tagged with `SPAN_KIND_RPC_SERVER` and any of `HTTP_URL`, `HTTP_METHOD` or `HTTP_STATUS_CODE`. 136 | Sample output: 137 | ``` 138 | # HELP http_request_handler_duration_seconds Duration of HTTP requests in second 139 | # TYPE http_request_handler_duration_seconds histogram 140 | http_request_handler_duration_seconds_bucket{le="0.005",parent_service="my-parent-service",method="GET",code="200",name="http_request" 0 141 | http_request_handler_duration_seconds_bucket{le="0.01",parent_service="my-parent-service",method="GET",code="200",name="http_request" 0 142 | http_request_handler_duration_seconds_bucket{le="0.025",parent_service="my-parent-service",method="GET",code="200",name="http_request" 0 143 | http_request_handler_duration_seconds_bucket{le="0.05",parent_service="my-parent-service",method="GET",code="200",name="http_request" 0 144 | http_request_handler_duration_seconds_bucket{le="0.1",parent_service="my-parent-service",method="GET",code="200",name="http_request" 1 145 | http_request_handler_duration_seconds_bucket{le="0.25",parent_service="my-parent-service",method="GET",code="200",name="http_request" 1 146 | http_request_handler_duration_seconds_bucket{le="0.5",parent_service="my-parent-service",method="GET",code="200",name="http_request" 2 147 | http_request_handler_duration_seconds_bucket{le="1",parent_service="my-parent-service",method="GET",code="200",name="http_request" 2 148 | http_request_handler_duration_seconds_bucket{le="2.5",parent_service="my-parent-service",method="GET",code="200",name="http_request" 2 149 | http_request_handler_duration_seconds_bucket{le="5",parent_service="my-parent-service",method="GET",code="200",name="http_request" 2 150 | http_request_handler_duration_seconds_bucket{le="10",parent_service="my-parent-service",method="GET",code="200",name="http_request" 2 151 | http_request_handler_duration_seconds_bucket{le="+Inf",parent_service="my-parent-service",method="GET",code="200",name="http_request" 2 152 | http_request_handler_duration_seconds_sum{parent_service="my-parent-service",method="GET",code="200",name="http_request" 0.4 153 | http_request_handler_duration_seconds_count{parent_service="my-parent-service",method="GET",code="200",name="http_request" 2 154 | ``` 155 | 156 | ## Future and ideas 157 | 158 | This library is new, in the future we could measure much more useful and specific metrics with it. 159 | Please share your ideas in a form of issues or pull-requests. 160 | -------------------------------------------------------------------------------- /src/reporters/PrometheusReporter.e2e.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const sinon = require('sinon') 4 | const dedent = require('dedent') 5 | const { expect } = require('chai') 6 | const { Tags } = require('opentracing') 7 | const { Tracer } = require('../tracer') 8 | const PrometheusReporter = require('./PrometheusReporter') 9 | 10 | describe('e2e: PrometheusReporter', () => { 11 | let clock 12 | 13 | beforeEach(() => { 14 | clock = sinon.useFakeTimers() 15 | }) 16 | 17 | afterEach(() => { 18 | clock.restore() 19 | }) 20 | 21 | describe('operation metrics', () => { 22 | it('should have operation metrics initialized', () => { 23 | const reporter = new PrometheusReporter() 24 | 25 | expect(reporter.metrics()).to.be.equal(dedent` 26 | # HELP operation_duration_seconds Duration of operations in second 27 | # TYPE operation_duration_seconds histogram\n 28 | `) 29 | }) 30 | 31 | it('should have operation metrics', () => { 32 | const reporter = new PrometheusReporter() 33 | const tracer = new Tracer('my-service', [reporter]) 34 | 35 | const span1 = tracer.startSpan('my-operation') 36 | clock.tick(100) 37 | span1.finish() 38 | 39 | const span2 = tracer.startSpan('my-operation') 40 | clock.tick(300) 41 | span2.finish() 42 | 43 | const labelStr = `parent_service="${PrometheusReporter.LABEL_PARENT_SERVICE_UNKNOWN}",name="my-operation"` 44 | 45 | expect(reporter.metrics()).to.be.equal(dedent` 46 | # HELP operation_duration_seconds Duration of operations in second 47 | # TYPE operation_duration_seconds histogram 48 | operation_duration_seconds_bucket{le="0.005",${labelStr}} 0 49 | operation_duration_seconds_bucket{le="0.01",${labelStr}} 0 50 | operation_duration_seconds_bucket{le="0.025",${labelStr}} 0 51 | operation_duration_seconds_bucket{le="0.05",${labelStr}} 0 52 | operation_duration_seconds_bucket{le="0.1",${labelStr}} 1 53 | operation_duration_seconds_bucket{le="0.25",${labelStr}} 1 54 | operation_duration_seconds_bucket{le="0.5",${labelStr}} 2 55 | operation_duration_seconds_bucket{le="1",${labelStr}} 2 56 | operation_duration_seconds_bucket{le="2.5",${labelStr}} 2 57 | operation_duration_seconds_bucket{le="5",${labelStr}} 2 58 | operation_duration_seconds_bucket{le="10",${labelStr}} 2 59 | operation_duration_seconds_bucket{le="+Inf",${labelStr}} 2 60 | operation_duration_seconds_sum{${labelStr}} 0.4 61 | operation_duration_seconds_count{${labelStr}} 2\n 62 | `) 63 | }) 64 | 65 | it('should have operation metrics with parent', () => { 66 | const reporter = new PrometheusReporter() 67 | const parentTracer = new Tracer('parent-service') 68 | const tracer = new Tracer('service', [reporter]) 69 | 70 | const parentSpan1 = parentTracer.startSpan('parent-operation') 71 | const span1 = tracer.startSpan('my-operation', { childOf: parentSpan1 }) 72 | clock.tick(100) 73 | span1.finish() 74 | 75 | const parentSpan2 = parentTracer.startSpan('parent-operation') 76 | const span2 = tracer.startSpan('my-operation', { childOf: parentSpan2 }) 77 | clock.tick(300) 78 | span2.finish() 79 | 80 | const labelStr = 'parent_service="parent-service",name="my-operation"' 81 | 82 | expect(reporter.metrics()).to.be.equal(dedent` 83 | # HELP operation_duration_seconds Duration of operations in second 84 | # TYPE operation_duration_seconds histogram 85 | operation_duration_seconds_bucket{le="0.005",${labelStr}} 0 86 | operation_duration_seconds_bucket{le="0.01",${labelStr}} 0 87 | operation_duration_seconds_bucket{le="0.025",${labelStr}} 0 88 | operation_duration_seconds_bucket{le="0.05",${labelStr}} 0 89 | operation_duration_seconds_bucket{le="0.1",${labelStr}} 1 90 | operation_duration_seconds_bucket{le="0.25",${labelStr}} 1 91 | operation_duration_seconds_bucket{le="0.5",${labelStr}} 2 92 | operation_duration_seconds_bucket{le="1",${labelStr}} 2 93 | operation_duration_seconds_bucket{le="2.5",${labelStr}} 2 94 | operation_duration_seconds_bucket{le="5",${labelStr}} 2 95 | operation_duration_seconds_bucket{le="10",${labelStr}} 2 96 | operation_duration_seconds_bucket{le="+Inf",${labelStr}} 2 97 | operation_duration_seconds_sum{${labelStr}} 0.4 98 | operation_duration_seconds_count{${labelStr}} 2\n 99 | `) 100 | }) 101 | }) 102 | 103 | describe('http_request_handler', () => { 104 | it('should have http_request_handler metrics', () => { 105 | const reporter = new PrometheusReporter({ 106 | ignoreTags: { 107 | [Tags.HTTP_URL]: /bar/ 108 | } 109 | }) 110 | const tracer = new Tracer('my-service', [reporter]) 111 | 112 | const span1 = tracer.startSpan('http_request') 113 | span1.setTag(Tags.HTTP_URL, 'http://127.0.0.1/foo') 114 | span1.setTag(Tags.HTTP_METHOD, 'GET') 115 | span1.setTag(Tags.HTTP_STATUS_CODE, 200) 116 | span1.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_RPC_SERVER) 117 | clock.tick(100) 118 | span1.finish() 119 | 120 | // will be ignored 121 | const span2 = tracer.startSpan('http_request') 122 | span2.setTag(Tags.HTTP_URL, 'http://127.0.0.1/bar') 123 | span2.setTag(Tags.HTTP_METHOD, 'GET') 124 | span2.setTag(Tags.HTTP_STATUS_CODE, 200) 125 | span2.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_RPC_SERVER) 126 | clock.tick(300) 127 | span2.finish() 128 | 129 | const labelStr1 = `parent_service="${PrometheusReporter.LABEL_PARENT_SERVICE_UNKNOWN}",name="http_request"` 130 | const labelStr2 = `parent_service="${PrometheusReporter.LABEL_PARENT_SERVICE_UNKNOWN}",method="GET",code="200"` 131 | 132 | expect(reporter.metrics()).to.be.equal(dedent` 133 | # HELP operation_duration_seconds Duration of operations in second 134 | # TYPE operation_duration_seconds histogram 135 | operation_duration_seconds_bucket{le="0.005",${labelStr1}} 0 136 | operation_duration_seconds_bucket{le="0.01",${labelStr1}} 0 137 | operation_duration_seconds_bucket{le="0.025",${labelStr1}} 0 138 | operation_duration_seconds_bucket{le="0.05",${labelStr1}} 0 139 | operation_duration_seconds_bucket{le="0.1",${labelStr1}} 1 140 | operation_duration_seconds_bucket{le="0.25",${labelStr1}} 1 141 | operation_duration_seconds_bucket{le="0.5",${labelStr1}} 1 142 | operation_duration_seconds_bucket{le="1",${labelStr1}} 1 143 | operation_duration_seconds_bucket{le="2.5",${labelStr1}} 1 144 | operation_duration_seconds_bucket{le="5",${labelStr1}} 1 145 | operation_duration_seconds_bucket{le="10",${labelStr1}} 1 146 | operation_duration_seconds_bucket{le="+Inf",${labelStr1}} 1 147 | operation_duration_seconds_sum{${labelStr1}} 0.1 148 | operation_duration_seconds_count{${labelStr1}} 1 149 | 150 | # HELP http_request_handler_duration_seconds Duration of HTTP requests in second 151 | # TYPE http_request_handler_duration_seconds histogram 152 | http_request_handler_duration_seconds_bucket{le="0.005",${labelStr2}} 0 153 | http_request_handler_duration_seconds_bucket{le="0.01",${labelStr2}} 0 154 | http_request_handler_duration_seconds_bucket{le="0.025",${labelStr2}} 0 155 | http_request_handler_duration_seconds_bucket{le="0.05",${labelStr2}} 0 156 | http_request_handler_duration_seconds_bucket{le="0.1",${labelStr2}} 1 157 | http_request_handler_duration_seconds_bucket{le="0.25",${labelStr2}} 1 158 | http_request_handler_duration_seconds_bucket{le="0.5",${labelStr2}} 1 159 | http_request_handler_duration_seconds_bucket{le="1",${labelStr2}} 1 160 | http_request_handler_duration_seconds_bucket{le="2.5",${labelStr2}} 1 161 | http_request_handler_duration_seconds_bucket{le="5",${labelStr2}} 1 162 | http_request_handler_duration_seconds_bucket{le="10",${labelStr2}} 1 163 | http_request_handler_duration_seconds_bucket{le="+Inf",${labelStr2}} 1 164 | http_request_handler_duration_seconds_sum{${labelStr2}} 0.1 165 | http_request_handler_duration_seconds_count{${labelStr2}} 1\n 166 | `) 167 | }) 168 | }) 169 | }) 170 | -------------------------------------------------------------------------------- /src/reporters/PrometheusReporter.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const sinon = require('sinon') 4 | const { expect } = require('chai') 5 | const dedent = require('dedent') 6 | const { Tags } = require('opentracing') 7 | const { Tracer } = require('../tracer') 8 | const PrometheusReporter = require('./PrometheusReporter') 9 | 10 | describe('reporter/PrometheusReporter', () => { 11 | let clock 12 | 13 | beforeEach(() => { 14 | clock = sinon.useFakeTimers() 15 | }) 16 | 17 | afterEach(() => { 18 | clock.restore() 19 | }) 20 | 21 | describe('#constructor', () => { 22 | it('should create a PrometheusReporter', () => { 23 | const prometheusReporter = new PrometheusReporter() 24 | 25 | expect(prometheusReporter).to.have.property('_registry') 26 | }) 27 | }) 28 | 29 | describe('#reportFinish', () => { 30 | it('should skip operation metrics by tag value', function () { 31 | // init 32 | const prometheusReporter = new PrometheusReporter({ 33 | ignoreTags: { 34 | [Tags.HTTP_URL]: /foo/ 35 | } 36 | }) 37 | const metricsOperationDurationSeconds = prometheusReporter._metricsOperationDurationSeconds() 38 | 39 | const metricsStub = { 40 | observe: this.sandbox.spy() 41 | } 42 | 43 | this.sandbox.stub(metricsOperationDurationSeconds, 'labels').callsFake(() => metricsStub) 44 | 45 | // generate data 46 | const tracer = new Tracer('service') 47 | 48 | const span = tracer.startSpan('my-operation') 49 | span.setTag(Tags.HTTP_URL, 'http://127.0.0.1/foo') 50 | clock.tick(100) 51 | span.finish() 52 | 53 | prometheusReporter.reportFinish(span) 54 | 55 | // assert 56 | expect(metricsOperationDurationSeconds.labels).to.have.callCount(0) 57 | expect(metricsStub.observe).to.have.callCount(0) 58 | }) 59 | 60 | it('should observe operation metrics without parent', function () { 61 | // init 62 | const prometheusReporter = new PrometheusReporter() 63 | const metricsOperationDurationSeconds = prometheusReporter._metricsOperationDurationSeconds() 64 | 65 | const metricsStub = { 66 | observe: this.sandbox.spy() 67 | } 68 | 69 | this.sandbox.stub(metricsOperationDurationSeconds, 'labels').callsFake(() => metricsStub) 70 | 71 | // generate data 72 | const tracer = new Tracer('service') 73 | 74 | const span = tracer.startSpan('my-operation') 75 | clock.tick(100) 76 | span.finish() 77 | 78 | prometheusReporter.reportFinish(span) 79 | 80 | // assert 81 | expect(metricsOperationDurationSeconds.labels).to.have.callCount(1) 82 | expect(metricsOperationDurationSeconds.labels).to.be.calledWith(PrometheusReporter.LABEL_PARENT_SERVICE_UNKNOWN) 83 | 84 | expect(metricsStub.observe).to.have.callCount(1) 85 | expect(metricsStub.observe).to.be.calledWith(0.1) 86 | }) 87 | 88 | it('should observe operation metrics with parent', function () { 89 | // init 90 | const prometheusReporter = new PrometheusReporter() 91 | const metricsOperationDurationSeconds = prometheusReporter._metricsOperationDurationSeconds() 92 | 93 | const metricsStub = { 94 | observe: this.sandbox.spy() 95 | } 96 | 97 | this.sandbox.stub(metricsOperationDurationSeconds, 'labels').callsFake(() => metricsStub) 98 | 99 | // generate data 100 | const parentTracer = new Tracer('parent-service') 101 | const tracer = new Tracer('service') 102 | 103 | const parentSpan1 = parentTracer.startSpan('parent-operation') 104 | const span1 = tracer.startSpan('my-operation', { childOf: parentSpan1 }) 105 | clock.tick(100) 106 | span1.finish() 107 | 108 | const parentSpan2 = parentTracer.startSpan('parent-operation') 109 | const span2 = tracer.startSpan('my-operation', { childOf: parentSpan2 }) 110 | clock.tick(300) 111 | span2.finish() 112 | 113 | prometheusReporter.reportFinish(span1) 114 | prometheusReporter.reportFinish(span2) 115 | 116 | // assert 117 | expect(metricsOperationDurationSeconds.labels).to.have.callCount(2) 118 | expect(metricsOperationDurationSeconds.labels).to.be.calledWith('parent-service') 119 | 120 | expect(metricsStub.observe).to.have.callCount(2) 121 | expect(metricsStub.observe).to.be.calledWith(0.1) 122 | expect(metricsStub.observe).to.be.calledWith(0.3) 123 | }) 124 | 125 | it('should observe HTTP request metrics without parent', function () { 126 | // init 127 | const prometheusReporter = new PrometheusReporter() 128 | const httpRequestDurationSeconds = prometheusReporter._metricshttpRequestDurationSeconds() 129 | 130 | const metricsStub = { 131 | observe: this.sandbox.spy() 132 | } 133 | 134 | this.sandbox.stub(httpRequestDurationSeconds, 'labels').callsFake(() => metricsStub) 135 | 136 | // generate data 137 | const tracer = new Tracer('service') 138 | 139 | const span = tracer.startSpan('http_request') 140 | span.setTag(Tags.HTTP_METHOD, 'GET') 141 | span.setTag(Tags.HTTP_STATUS_CODE, 200) 142 | span.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_RPC_SERVER) 143 | clock.tick(100) 144 | span.finish() 145 | 146 | prometheusReporter.reportFinish(span) 147 | 148 | // assert 149 | expect(httpRequestDurationSeconds.labels).to.have.callCount(1) 150 | expect(httpRequestDurationSeconds.labels) 151 | .to.be.calledWith(PrometheusReporter.LABEL_PARENT_SERVICE_UNKNOWN, 'GET', 200) 152 | 153 | expect(metricsStub.observe).to.have.callCount(1) 154 | expect(metricsStub.observe).to.be.calledWith(0.1) 155 | }) 156 | 157 | it('should observe HTTP request metrics with parent', function () { 158 | // init 159 | const prometheusReporter = new PrometheusReporter() 160 | const httpRequestDurationSeconds = prometheusReporter._metricshttpRequestDurationSeconds() 161 | 162 | const metricsStub = { 163 | observe: this.sandbox.spy() 164 | } 165 | 166 | this.sandbox.stub(httpRequestDurationSeconds, 'labels').callsFake(() => metricsStub) 167 | 168 | // generate data 169 | const parentTracer = new Tracer('parent-service') 170 | const tracer = new Tracer('service') 171 | 172 | const parentSpan1 = parentTracer.startSpan('parent-operation') 173 | const span1 = tracer.startSpan('http_request', { childOf: parentSpan1 }) 174 | span1.setTag(Tags.HTTP_METHOD, 'GET') 175 | span1.setTag(Tags.HTTP_STATUS_CODE, 200) 176 | span1.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_RPC_SERVER) 177 | clock.tick(100) 178 | span1.finish() 179 | 180 | const parentSpan2 = parentTracer.startSpan('parent-operation') 181 | const span2 = tracer.startSpan('http_request', { childOf: parentSpan2 }) 182 | span2.setTag(Tags.HTTP_METHOD, 'POST') 183 | span2.setTag(Tags.HTTP_STATUS_CODE, 201) 184 | span2.setTag(Tags.SPAN_KIND, Tags.SPAN_KIND_RPC_SERVER) 185 | clock.tick(300) 186 | span2.finish() 187 | 188 | prometheusReporter.reportFinish(span1) 189 | prometheusReporter.reportFinish(span2) 190 | 191 | // assert 192 | expect(httpRequestDurationSeconds.labels).to.have.callCount(2) 193 | expect(httpRequestDurationSeconds.labels).to.be.calledWith('parent-service', 'POST', 201) 194 | 195 | expect(metricsStub.observe).to.have.callCount(2) 196 | expect(metricsStub.observe).to.be.calledWith(0.1) 197 | expect(metricsStub.observe).to.be.calledWith(0.3) 198 | }) 199 | 200 | it('should skip client HTTP requests', function () { 201 | // init 202 | const prometheusReporter = new PrometheusReporter() 203 | const httpRequestDurationSeconds = prometheusReporter._metricshttpRequestDurationSeconds() 204 | 205 | const metricsStub = { 206 | observe: this.sandbox.spy() 207 | } 208 | 209 | this.sandbox.stub(httpRequestDurationSeconds, 'labels').callsFake(() => metricsStub) 210 | 211 | // generate data 212 | const tracer = new Tracer('service') 213 | 214 | const span = tracer.startSpan('http_request') 215 | span.setTag(Tags.HTTP_METHOD, 'GET') 216 | span.setTag(Tags.HTTP_STATUS_CODE, 200) 217 | span.setTag(Tags.SPAN_KIND_RPC_SERVER, false) // or not set 218 | clock.tick(100) 219 | span.finish() 220 | 221 | prometheusReporter.reportFinish(span) 222 | 223 | // assert 224 | expect(httpRequestDurationSeconds.labels).to.have.callCount(0) 225 | expect(metricsStub.observe).to.have.callCount(0) 226 | }) 227 | }) 228 | 229 | describe('#metrics', () => { 230 | it('should have operation metrics initialized', () => { 231 | const reporter = new PrometheusReporter() 232 | 233 | expect(reporter.metrics()).to.be.equal(dedent` 234 | # HELP operation_duration_seconds Duration of operations in second 235 | # TYPE operation_duration_seconds histogram\n 236 | `) 237 | }) 238 | }) 239 | }) 240 | --------------------------------------------------------------------------------