diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc index f2ca0f0c2..a173d8161 100644 --- a/docs/configuration.asciidoc +++ b/docs/configuration.asciidoc @@ -218,10 +218,15 @@ _Default:_ `null` _Default:_ `{}` |`context` -|`object` - A custom object that you can use for observability in yoru events. +|`object` - A custom object that you can use for observability in your events. It will be merged with the API level context option. + _Default:_ `null` +|`enableMetaHeader` +|`boolean` - If true, adds an header named `'x-elastic-client-meta'`, containing some minimal telemetry data, +such as the client and platform version. + +_Default:_ `true` + |`cloud` a|`object` - Custom configuration for connecting to https://cloud.elastic.co[Elastic Cloud]. See https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/auth-reference.html[Authentication] diff --git a/index.d.ts b/index.d.ts index 4f309d6aa..5f2499a0e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -108,6 +108,7 @@ interface ClientOptions { auth?: BasicAuth | ApiKeyAuth; context?: Context; proxy?: string | URL; + enableMetaHeader?: boolean; cloud?: { id: string; // TODO: remove username and password here in 8 diff --git a/index.js b/index.js index e4d1ae704..2f1d84499 100644 --- a/index.js +++ b/index.js @@ -33,6 +33,8 @@ const Serializer = require('./lib/Serializer') const errors = require('./lib/errors') const { ConfigurationError } = errors const { prepareHeaders } = Connection.internals +const clientVersion = require('./package.json').version +const nodeVersion = process.versions.node const kInitialOptions = Symbol('elasticsearchjs-initial-options') const kChild = Symbol('elasticsearchjs-child') @@ -125,13 +127,18 @@ class Client extends ESAPI { auth: null, opaqueIdPrefix: null, context: null, - proxy: null + proxy: null, + enableMetaHeader: true }, opts) this[kInitialOptions] = options this[kExtensions] = [] this.name = options.name + if (options.enableMetaHeader) { + options.headers['x-elastic-client-meta'] = `es=${clientVersion},js=${nodeVersion},t=${clientVersion},hc=${nodeVersion}` + } + if (opts[kChild] !== undefined) { this.serializer = options[kChild].serializer this.connectionPool = options[kChild].connectionPool @@ -179,7 +186,13 @@ class Client extends ESAPI { /* istanbul ignore else */ if (Helpers !== null) { - this.helpers = new Helpers({ client: this, maxRetries: options.maxRetries }) + this.helpers = new Helpers({ + client: this, + maxRetries: options.maxRetries, + metaHeader: options.enableMetaHeader + ? `es=${clientVersion},js=${nodeVersion},t=${clientVersion},hc=${nodeVersion}` + : null + }) } } diff --git a/lib/Helpers.js b/lib/Helpers.js index 6a3524b5f..bd0eeea0a 100644 --- a/lib/Helpers.js +++ b/lib/Helpers.js @@ -28,12 +28,14 @@ const { ResponseError, ConfigurationError } = require('./errors') const pImmediate = promisify(setImmediate) const sleep = promisify(setTimeout) const kClient = Symbol('elasticsearch-client') +const kMetaHeader = Symbol('meta header') /* istanbul ignore next */ const noop = () => {} class Helpers { constructor (opts) { this[kClient] = opts.client + this[kMetaHeader] = opts.metaHeader this.maxRetries = opts.maxRetries } @@ -71,6 +73,10 @@ class Helpers { * @return {iterator} the async iterator */ async * scrollSearch (params, options = {}) { + if (this[kMetaHeader] !== null) { + options.headers = options.headers || {} + options.headers['x-elastic-client-meta'] = this[kMetaHeader] + ',h=s' + } // TODO: study scroll search slices const wait = options.wait || 5000 const maxRetries = options.maxRetries || this.maxRetries @@ -99,7 +105,7 @@ class Helpers { stop = true await this[kClient].clearScroll( { body: { scroll_id } }, - { ignore: [400] } + { ignore: [400], ...options } ) } @@ -414,6 +420,7 @@ class Helpers { bulk (options) { const client = this[kClient] const { serialize, deserialize } = client.serializer + const reqOptions = this[kMetaHeader] !== null ? { headers: { 'x-elastic-client-meta': this[kMetaHeader] + ',h=bp' } } : {} const { datasource, onDocument, @@ -676,7 +683,7 @@ class Helpers { function tryBulk (bulkBody, callback) { if (shouldAbort === true) return callback(null, []) - client.bulk(Object.assign({}, bulkOptions, { body: bulkBody }), (err, { body }) => { + client.bulk(Object.assign({}, bulkOptions, { body: bulkBody }), reqOptions, (err, { body }) => { if (err) return callback(err, null) if (body.errors === false) { stats.successful += body.items.length diff --git a/lib/Transport.js b/lib/Transport.js index 50b75ccd5..46c1e3986 100644 --- a/lib/Transport.js +++ b/lib/Transport.js @@ -44,6 +44,7 @@ class Transport { if (typeof opts.compression === 'string' && opts.compression !== 'gzip') { throw new ConfigurationError(`Invalid compression: '${opts.compression}'`) } + this.emit = opts.emit this.connectionPool = opts.connectionPool this.serializer = opts.serializer diff --git a/test/unit/client.test.js b/test/unit/client.test.js index 151f08beb..6c3e754db 100644 --- a/test/unit/client.test.js +++ b/test/unit/client.test.js @@ -26,6 +26,8 @@ const intoStream = require('into-stream') const { Client, ConnectionPool, Transport, Connection, errors } = require('../../index') const { CloudConnectionPool } = require('../../lib/pool') const { buildServer } = require('../utils') +const clientVersion = require('../../package.json').version +const nodeVersion = process.versions.node test('Configure host', t => { t.test('Single string', t => { @@ -1300,3 +1302,62 @@ test('Content length too big (string)', t => { t.strictEqual(result.meta.attempts, 0) }) }) + +test('Meta header enabled', t => { + t.plan(2) + + class MockConnection extends Connection { + request (params, callback) { + t.match(params.headers, { 'x-elastic-client-meta': `es=${clientVersion},js=${nodeVersion},t=${clientVersion},hc=${nodeVersion}` }) + const stream = intoStream(JSON.stringify({ hello: 'world' })) + stream.statusCode = 200 + stream.headers = { + 'content-type': 'application/json;utf=8', + 'content-length': '17', + connection: 'keep-alive', + date: new Date().toISOString() + } + process.nextTick(callback, null, stream) + return { abort () {} } + } + } + + const client = new Client({ + node: 'http://localhost:9200', + Connection: MockConnection + }) + + client.info((err, result) => { + t.error(err) + }) +}) + +test('Meta header disabled', t => { + t.plan(2) + + class MockConnection extends Connection { + request (params, callback) { + t.notMatch(params.headers, { 'x-elastic-client-meta': `es=${clientVersion},js=${nodeVersion},t=${clientVersion},hc=${nodeVersion}` }) + const stream = intoStream(JSON.stringify({ hello: 'world' })) + stream.statusCode = 200 + stream.headers = { + 'content-type': 'application/json;utf=8', + 'content-length': '17', + connection: 'keep-alive', + date: new Date().toISOString() + } + process.nextTick(callback, null, stream) + return { abort () {} } + } + } + + const client = new Client({ + node: 'http://localhost:9200', + Connection: MockConnection, + enableMetaHeader: false + }) + + client.info((err, result) => { + t.error(err) + }) +}) diff --git a/test/unit/helpers/bulk.test.js b/test/unit/helpers/bulk.test.js index c0999e02a..46f1096e9 100644 --- a/test/unit/helpers/bulk.test.js +++ b/test/unit/helpers/bulk.test.js @@ -27,6 +27,8 @@ const semver = require('semver') const { test } = require('tap') const { Client, errors } = require('../../../') const { buildServer, connection } = require('../../utils') +const clientVersion = require('../../../package.json').version +const nodeVersion = process.versions.node const dataset = [ { user: 'jon', age: 23 }, @@ -41,7 +43,10 @@ test('bulk index', t => { const MockConnection = connection.buildMockConnection({ onRequest (params) { t.strictEqual(params.path, '/_bulk') - t.match(params.headers, { 'content-type': 'application/x-ndjson' }) + t.match(params.headers, { + 'content-type': 'application/x-ndjson', + 'x-elastic-client-meta': `es=${clientVersion},js=${nodeVersion},t=${clientVersion},hc=${nodeVersion},h=bp` + }) const [action, payload] = params.body.split('\n') t.deepEqual(JSON.parse(action), { index: { _index: 'test' } }) t.deepEqual(JSON.parse(payload), dataset[count++]) @@ -84,6 +89,9 @@ test('bulk index', t => { onRequest (params) { t.strictEqual(params.path, '/_bulk') t.match(params.headers, { 'content-type': 'application/x-ndjson' }) + t.notMatch(params.headers, { + 'x-elastic-client-meta': `es=${clientVersion},js=${nodeVersion},t=${clientVersion},hc=${nodeVersion},h=bp` + }) const [action, payload] = params.body.split('\n') t.deepEqual(JSON.parse(action), { index: { _index: 'test' } }) t.deepEqual(JSON.parse(payload), dataset[count++]) @@ -93,7 +101,8 @@ test('bulk index', t => { const client = new Client({ node: 'http://localhost:9200', - Connection: MockConnection + Connection: MockConnection, + enableMetaHeader: false }) const result = await client.helpers.bulk({ datasource: dataset.slice(), diff --git a/test/unit/helpers/scroll.test.js b/test/unit/helpers/scroll.test.js index ca50957ee..0de2e12cd 100644 --- a/test/unit/helpers/scroll.test.js +++ b/test/unit/helpers/scroll.test.js @@ -22,11 +22,17 @@ const { test } = require('tap') const { Client, errors } = require('../../../') const { connection } = require('../../utils') +const clientVersion = require('../../../package.json').version +const nodeVersion = process.versions.node test('Scroll search', async t => { var count = 0 const MockConnection = connection.buildMockConnection({ onRequest (params) { + t.match(params.headers, { + 'x-elastic-client-meta': `es=${clientVersion},js=${nodeVersion},t=${clientVersion},hc=${nodeVersion},h=s` + }) + count += 1 if (params.method === 'POST') { t.strictEqual(params.querystring, 'scroll=1m') @@ -73,6 +79,9 @@ test('Clear a scroll search', async t => { var count = 0 const MockConnection = connection.buildMockConnection({ onRequest (params) { + t.notMatch(params.headers, { + 'x-elastic-client-meta': `es=${clientVersion},js=${nodeVersion},t=${clientVersion},hc=${nodeVersion},h=s` + }) if (params.method === 'DELETE') { const body = JSON.parse(params.body) t.strictEqual(body.scroll_id, 'id') @@ -95,7 +104,8 @@ test('Clear a scroll search', async t => { const client = new Client({ node: 'http://localhost:9200', - Connection: MockConnection + Connection: MockConnection, + enableMetaHeader: false }) const scrollSearch = client.helpers.scrollSearch({