├── .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 | [](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 |
--------------------------------------------------------------------------------