[Elasticsearch] Redact logs from known APIs (#153049)

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Alejandro Fernández Haro 2023-03-18 10:50:09 +01:00 committed by GitHub
parent e6d03fe356
commit 5142d73243
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 535 additions and 38 deletions

View file

@ -10,7 +10,11 @@ export { ScopedClusterClient } from './src/scoped_cluster_client';
export { ClusterClient } from './src/cluster_client'; export { ClusterClient } from './src/cluster_client';
export { configureClient } from './src/configure_client'; export { configureClient } from './src/configure_client';
export { type AgentStatsProvider, AgentManager, type NetworkAgent } from './src/agent_manager'; export { type AgentStatsProvider, AgentManager, type NetworkAgent } from './src/agent_manager';
export { getRequestDebugMeta, getErrorMessage } from './src/log_query_and_deprecation'; export {
type RequestDebugMeta,
getRequestDebugMeta,
getErrorMessage,
} from './src/log_query_and_deprecation';
export { export {
PRODUCT_RESPONSE_HEADER, PRODUCT_RESPONSE_HEADER,
DEFAULT_HEADERS, DEFAULT_HEADERS,

View file

@ -212,6 +212,7 @@ describe('configureClient', () => {
logger, logger,
client, client,
type: 'test', type: 'test',
apisToRedactInLogs: [],
}); });
}); });
}); });

View file

@ -45,7 +45,8 @@ export const configureClient = (
ConnectionPool: ClusterConnectionPool, ConnectionPool: ClusterConnectionPool,
}); });
instrumentEsQueryAndDeprecationLogger({ logger, client, type }); const { apisToRedactInLogs = [] } = config;
instrumentEsQueryAndDeprecationLogger({ logger, client, type, apisToRedactInLogs });
return client; return client;
}; };

View file

@ -96,13 +96,23 @@ describe('instrumentQueryAndDeprecationLogger', () => {
} }
it('creates a query logger context based on the `type` parameter', () => { it('creates a query logger context based on the `type` parameter', () => {
instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test123' }); instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test123',
apisToRedactInLogs: [],
});
expect(logger.get).toHaveBeenCalledWith('query', 'test123'); expect(logger.get).toHaveBeenCalledWith('query', 'test123');
}); });
describe('logs each query', () => { describe('logs each query', () => {
it('when request body is an object', () => { it('when request body is an object', () => {
instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [],
});
const response = createResponseWithBody({ const response = createResponseWithBody({
seq_no_primary_term: true, seq_no_primary_term: true,
@ -120,7 +130,12 @@ describe('instrumentQueryAndDeprecationLogger', () => {
}); });
it('when request body is a string', () => { it('when request body is a string', () => {
instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [],
});
const response = createResponseWithBody( const response = createResponseWithBody(
JSON.stringify({ JSON.stringify({
@ -140,7 +155,12 @@ describe('instrumentQueryAndDeprecationLogger', () => {
}); });
it('when request body is a buffer', () => { it('when request body is a buffer', () => {
instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [],
});
const response = createResponseWithBody( const response = createResponseWithBody(
Buffer.from( Buffer.from(
@ -162,7 +182,12 @@ describe('instrumentQueryAndDeprecationLogger', () => {
}); });
it('when request body is a readable stream', () => { it('when request body is a readable stream', () => {
instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [],
});
const response = createResponseWithBody( const response = createResponseWithBody(
Readable.from( Readable.from(
@ -184,7 +209,12 @@ describe('instrumentQueryAndDeprecationLogger', () => {
}); });
it('when request body is not defined', () => { it('when request body is not defined', () => {
instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [],
});
const response = createResponseWithBody(); const response = createResponseWithBody();
@ -196,7 +226,12 @@ describe('instrumentQueryAndDeprecationLogger', () => {
}); });
it('properly encode queries', () => { it('properly encode queries', () => {
instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [],
});
const response = createApiResponse({ const response = createApiResponse({
body: {}, body: {},
@ -217,7 +252,12 @@ describe('instrumentQueryAndDeprecationLogger', () => {
}); });
it('logs queries even in case of errors', () => { it('logs queries even in case of errors', () => {
instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [],
});
const response = createApiResponse({ const response = createApiResponse({
statusCode: 500, statusCode: 500,
@ -248,7 +288,12 @@ describe('instrumentQueryAndDeprecationLogger', () => {
}); });
it('logs debug when the client emits an @elastic/elasticsearch error', () => { it('logs debug when the client emits an @elastic/elasticsearch error', () => {
instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [],
});
const response = createApiResponse({ body: {} }); const response = createApiResponse({ body: {} });
client.diagnostic.emit('response', new errors.TimeoutError('message', response), response); client.diagnostic.emit('response', new errors.TimeoutError('message', response), response);
@ -259,7 +304,12 @@ describe('instrumentQueryAndDeprecationLogger', () => {
}); });
it('logs debug when the client emits an ResponseError returned by elasticsearch', () => { it('logs debug when the client emits an ResponseError returned by elasticsearch', () => {
instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [],
});
const response = createApiResponse({ const response = createApiResponse({
statusCode: 400, statusCode: 400,
@ -285,7 +335,12 @@ describe('instrumentQueryAndDeprecationLogger', () => {
}); });
it('logs default error info when the error response body is empty', () => { it('logs default error info when the error response body is empty', () => {
instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [],
});
let response: DiagnosticResult<any, any> = createApiResponse({ let response: DiagnosticResult<any, any> = createApiResponse({
statusCode: 400, statusCode: 400,
@ -325,7 +380,12 @@ describe('instrumentQueryAndDeprecationLogger', () => {
}); });
it('adds meta information to logs', () => { it('adds meta information to logs', () => {
instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [],
});
let response = createApiResponse({ let response = createApiResponse({
statusCode: 400, statusCode: 400,
@ -407,7 +467,12 @@ describe('instrumentQueryAndDeprecationLogger', () => {
}); });
it('logs response size', () => { it('logs response size', () => {
instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [],
});
const response = createResponseWithBody( const response = createResponseWithBody(
{ {
@ -432,7 +497,12 @@ describe('instrumentQueryAndDeprecationLogger', () => {
describe('deprecation warnings from response headers', () => { describe('deprecation warnings from response headers', () => {
it('does not log when no deprecation warning header is returned', () => { it('does not log when no deprecation warning header is returned', () => {
instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [],
});
const response = createApiResponse({ const response = createApiResponse({
statusCode: 200, statusCode: 200,
@ -458,7 +528,12 @@ describe('instrumentQueryAndDeprecationLogger', () => {
}); });
it('does not log when warning header comes from a warn-agent that is not elasticsearch', () => { it('does not log when warning header comes from a warn-agent that is not elasticsearch', () => {
instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [],
});
const response = createApiResponse({ const response = createApiResponse({
statusCode: 200, statusCode: 200,
@ -487,7 +562,12 @@ describe('instrumentQueryAndDeprecationLogger', () => {
}); });
it('logs error when the client receives an Elasticsearch error response for a deprecated request originating from a user', () => { it('logs error when the client receives an Elasticsearch error response for a deprecated request originating from a user', () => {
instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [],
});
const response = createApiResponse({ const response = createApiResponse({
statusCode: 400, statusCode: 400,
@ -519,7 +599,12 @@ describe('instrumentQueryAndDeprecationLogger', () => {
}); });
it('logs warning when the client receives an Elasticsearch error response for a deprecated request originating from kibana', () => { it('logs warning when the client receives an Elasticsearch error response for a deprecated request originating from kibana', () => {
instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [],
});
const response = createApiResponse({ const response = createApiResponse({
statusCode: 400, statusCode: 400,
@ -552,7 +637,12 @@ describe('instrumentQueryAndDeprecationLogger', () => {
}); });
it('logs error when the client receives an Elasticsearch success response for a deprecated request originating from a user', () => { it('logs error when the client receives an Elasticsearch success response for a deprecated request originating from a user', () => {
instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [],
});
const response = createApiResponse({ const response = createApiResponse({
statusCode: 200, statusCode: 200,
@ -584,7 +674,12 @@ describe('instrumentQueryAndDeprecationLogger', () => {
}); });
it('logs warning when the client receives an Elasticsearch success response for a deprecated request originating from kibana', () => { it('logs warning when the client receives an Elasticsearch success response for a deprecated request originating from kibana', () => {
instrumentEsQueryAndDeprecationLogger({ logger, client, type: 'test type' }); instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [],
});
const response = createApiResponse({ const response = createApiResponse({
statusCode: 200, statusCode: 200,
@ -616,5 +711,287 @@ describe('instrumentQueryAndDeprecationLogger', () => {
/Query:\n.*200\n.*GET \/_path\?hello\=dolly/ /Query:\n.*200\n.*GET \/_path\?hello\=dolly/
); );
}); });
describe('Request body redaction on some APIs', () => {
it('redacts for an API in the extended list (path only)', () => {
instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [{ path: '/foo' }],
});
const response = createApiResponse({
body: {},
statusCode: 200,
headers: {},
params: {
method: 'GET',
path: '/foo',
querystring: { hello: 'dolly' },
body: {
seq_no_primary_term: true,
query: {
term: { user: 'kimchy' },
},
},
},
});
client.diagnostic.emit('response', null, response);
expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(`
"200
GET /foo?hello=dolly
[redacted]"
`);
});
it('redacts for an API that is contained by the declared path (path only)', () => {
instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [{ path: '/foo' }],
});
const response = createApiResponse({
body: {},
statusCode: 200,
headers: {},
params: {
method: 'GET',
path: '/foo/something/something-else',
querystring: { hello: 'dolly' },
body: {
seq_no_primary_term: true,
query: {
term: { user: 'kimchy' },
},
},
},
});
client.diagnostic.emit('response', null, response);
expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(`
"200
GET /foo/something/something-else?hello=dolly
[redacted]"
`);
});
it('redacts for an API in the extended list (method and path)', () => {
instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [{ method: 'GET', path: '/foo' }],
});
const response = createApiResponse({
body: {},
statusCode: 200,
headers: {},
params: {
method: 'GET',
path: '/foo',
querystring: { hello: 'dolly' },
body: {
seq_no_primary_term: true,
query: {
term: { user: 'kimchy' },
},
},
},
});
client.diagnostic.emit('response', null, response);
expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(`
"200
GET /foo?hello=dolly
[redacted]"
`);
});
it('does not redact for an API in the extended list when method does not match', () => {
instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [{ method: 'PUT', path: '/foo' }],
});
const response = createApiResponse({
body: {},
statusCode: 200,
headers: {},
params: {
method: 'GET',
path: '/foo',
querystring: { hello: 'dolly' },
body: {
seq_no_primary_term: true,
query: {
term: { user: 'kimchy' },
},
},
},
});
client.diagnostic.emit('response', null, response);
expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(`
"200
GET /foo?hello=dolly
{\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}}"
`);
});
it('does not redact for an API in the extended list when path does not match', () => {
instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [{ path: '/foo' }],
});
const response = createApiResponse({
body: {},
statusCode: 200,
headers: {},
params: {
method: 'GET',
path: '/bar',
querystring: { hello: 'dolly' },
body: {
seq_no_primary_term: true,
query: {
term: { user: 'kimchy' },
},
},
},
});
client.diagnostic.emit('response', null, response);
expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(`
"200
GET /bar?hello=dolly
{\\"seq_no_primary_term\\":true,\\"query\\":{\\"term\\":{\\"user\\":\\"kimchy\\"}}}"
`);
});
describe('Known list', () => {
beforeEach(() => {
instrumentEsQueryAndDeprecationLogger({
logger,
client,
type: 'test type',
apisToRedactInLogs: [],
});
});
function createResponseWithPath(path: string, method: string = '*') {
return createApiResponse({
body: {},
statusCode: 200,
headers: {},
params: {
method,
path,
querystring: { hello: 'dolly' },
body: { super_secret: 'stuff' },
},
});
}
it('[*] /_security/', () => {
const response = createResponseWithPath('/_security/something');
client.diagnostic.emit('response', null, response);
expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(`
"200
* /_security/something?hello=dolly
[redacted]"
`);
});
it('[*] /_xpack/security/', () => {
const response = createResponseWithPath('/_xpack/security/something');
client.diagnostic.emit('response', null, response);
expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(`
"200
* /_xpack/security/something?hello=dolly
[redacted]"
`);
});
it('[POST] /_reindex', () => {
const response = createResponseWithPath('/_reindex', 'POST');
client.diagnostic.emit('response', null, response);
expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(`
"200
POST /_reindex?hello=dolly
[redacted]"
`);
});
it('[PUT] /_watcher/watch', () => {
const response = createResponseWithPath('/_watcher/watch', 'PUT');
client.diagnostic.emit('response', null, response);
expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(`
"200
PUT /_watcher/watch?hello=dolly
[redacted]"
`);
});
it('[PUT] /_xpack/watcher/watch', () => {
const response = createResponseWithPath('/_xpack/watcher/watch', 'PUT');
client.diagnostic.emit('response', null, response);
expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(`
"200
PUT /_xpack/watcher/watch?hello=dolly
[redacted]"
`);
});
it('[PUT] /_snapshot/something', () => {
const response = createResponseWithPath('/_snapshot/something', 'PUT');
client.diagnostic.emit('response', null, response);
expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(`
"200
PUT /_snapshot/something?hello=dolly
[redacted]"
`);
});
it('[PUT] /_logstash/pipeline/something', () => {
const response = createResponseWithPath('/_logstash/pipeline/something', 'PUT');
client.diagnostic.emit('response', null, response);
expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(`
"200
PUT /_logstash/pipeline/something?hello=dolly
[redacted]"
`);
});
it('[POST] /_nodes/reload_secure_settings', () => {
const response = createResponseWithPath('/_nodes/reload_secure_settings', 'POST');
client.diagnostic.emit('response', null, response);
expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(`
"200
POST /_nodes/reload_secure_settings?hello=dolly
[redacted]"
`);
});
it('[POST] /_nodes/*/reload_secure_settings', () => {
const response = createResponseWithPath('/_nodes/node-id/reload_secure_settings', 'POST');
client.diagnostic.emit('response', null, response);
expect(loggingSystemMock.collect(logger).debug[0][0]).toMatchInlineSnapshot(`
"200
POST /_nodes/node-id/reload_secure_settings?hello=dolly
[redacted]"
`);
});
});
});
}); });
}); });

View file

@ -13,8 +13,62 @@ import { errors, DiagnosticResult, RequestBody, Client } from '@elastic/elastics
import numeral from '@elastic/numeral'; import numeral from '@elastic/numeral';
import type { Logger } from '@kbn/logging'; import type { Logger } from '@kbn/logging';
import type { ElasticsearchErrorDetails } from '@kbn/es-errors'; import type { ElasticsearchErrorDetails } from '@kbn/es-errors';
import type { ElasticsearchApiToRedactInLogs } from '@kbn/core-elasticsearch-server';
import { getEcsResponseLog } from './get_ecs_response_log'; import { getEcsResponseLog } from './get_ecs_response_log';
/**
* The logger-relevant request meta of an ES request
*/
export interface RequestDebugMeta {
/**
* The requested method
*/
method: string;
/**
* The requested endpoint + querystring
*/
url: string;
/**
* The request body (it may be redacted)
*/
body: string;
/**
* The status code of the response
*/
statusCode: number | null;
}
/**
* Known list of APIs that should redact the request body in the logs
*/
const APIS_TO_REDACT_IN_LOGS: ElasticsearchApiToRedactInLogs[] = [
{ path: '/_security/' },
{ path: '/_xpack/security/' },
{ method: 'POST', path: '/_reindex' },
{ method: 'PUT', path: '/_watcher/watch' },
{ method: 'PUT', path: '/_xpack/watcher/watch' },
{ method: 'PUT', path: '/_snapshot/' },
{ method: 'PUT', path: '/_logstash/pipeline/' },
{ method: 'POST', path: '/_nodes/reload_secure_settings' },
{ method: 'POST', path: /\/_nodes\/.+\/reload_secure_settings/ },
];
function shouldRedactBodyInLogs(
requestDebugMeta: RequestDebugMeta,
extendedList: ElasticsearchApiToRedactInLogs[] = []
) {
return [...APIS_TO_REDACT_IN_LOGS, ...extendedList].some(({ path, method }) => {
if (!method || method === requestDebugMeta.method) {
if (typeof path === 'string') {
return requestDebugMeta.url.includes(path);
} else {
return path.test(requestDebugMeta.url);
}
}
return false;
});
}
const convertQueryString = (qs: string | Record<string, any> | undefined): string => { const convertQueryString = (qs: string | Record<string, any> | undefined): string => {
if (qs === undefined || typeof qs === 'string') { if (qs === undefined || typeof qs === 'string') {
return qs ?? ''; return qs ?? '';
@ -58,35 +112,43 @@ function getContentLength(headers?: IncomingHttpHeaders): number | undefined {
* *
* so it could be copy-pasted into the Dev console * so it could be copy-pasted into the Dev console
*/ */
function getResponseMessage(event: DiagnosticResult, bytesMsg: string): string { function getResponseMessage(
const errorMeta = getRequestDebugMeta(event); event: DiagnosticResult,
const body = errorMeta.body ? `\n${errorMeta.body}` : ''; bytesMsg: string,
return `${errorMeta.statusCode}${bytesMsg}\n${errorMeta.method} ${errorMeta.url}${body}`; apisToRedactInLogs: ElasticsearchApiToRedactInLogs[]
): string {
const debugMeta = getRequestDebugMeta(event, apisToRedactInLogs);
const body = debugMeta.body ? `\n${debugMeta.body}` : '';
return `${debugMeta.statusCode}${bytesMsg}\n${debugMeta.method} ${debugMeta.url}${body}`;
} }
/** /**
* Returns stringified debug information from an Elasticsearch request event * Returns stringified debug information from an Elasticsearch request event
* useful for logging in case of an unexpected failure. * useful for logging in case of an unexpected failure.
*/ */
export function getRequestDebugMeta(event: DiagnosticResult): { export function getRequestDebugMeta(
url: string; event: DiagnosticResult,
body: string; apisToRedactInLogs?: ElasticsearchApiToRedactInLogs[]
statusCode: number | null; ): RequestDebugMeta {
method: string;
} {
const params = event.meta.request.params; const params = event.meta.request.params;
// definition is wrong, `params.querystring` can be either a string or an object // definition is wrong, `params.querystring` can be either a string or an object
const querystring = convertQueryString(params.querystring); const querystring = convertQueryString(params.querystring);
return {
const debugMeta: RequestDebugMeta = {
url: `${params.path}${querystring ? `?${querystring}` : ''}`, url: `${params.path}${querystring ? `?${querystring}` : ''}`,
body: params.body ? `${ensureString(params.body)}` : '', body: params.body ? `${ensureString(params.body)}` : '',
method: params.method, method: params.method,
statusCode: event.statusCode!, statusCode: event.statusCode!,
}; };
// Some known APIs may contain sensitive information in the request body that we don't want to expose to the logs.
return shouldRedactBodyInLogs(debugMeta, apisToRedactInLogs)
? { ...debugMeta, body: '[redacted]' }
: debugMeta;
} }
/** HTTP Warning headers have the following syntax: /** HTTP Warning headers have the following syntax:
* <warn-code> <warn-agent> <warn-text> (where warn-code is a three digit number) * <warn-code> <warn-agent> <warn-text> (where warn-code is a three-digit number)
* This function tests if a warning comes from an Elasticsearch warn-agent * This function tests if a warning comes from an Elasticsearch warn-agent
* */ * */
const isEsWarning = (warning: string) => /\d\d\d Elasticsearch-/.test(warning); const isEsWarning = (warning: string) => /\d\d\d Elasticsearch-/.test(warning);
@ -95,10 +157,12 @@ export const instrumentEsQueryAndDeprecationLogger = ({
logger, logger,
client, client,
type, type,
apisToRedactInLogs,
}: { }: {
logger: Logger; logger: Logger;
client: Client; client: Client;
type: string; type: string;
apisToRedactInLogs: ElasticsearchApiToRedactInLogs[];
}) => { }) => {
const queryLogger = logger.get('query', type); const queryLogger = logger.get('query', type);
const deprecationLogger = logger.get('deprecation'); const deprecationLogger = logger.get('deprecation');
@ -111,12 +175,14 @@ export const instrumentEsQueryAndDeprecationLogger = ({
let queryMsg = ''; let queryMsg = '';
if (error) { if (error) {
if (error instanceof errors.ResponseError) { if (error instanceof errors.ResponseError) {
queryMsg = `${getResponseMessage(event, bytesMsg)} ${getErrorMessage(error)}`; queryMsg = `${getResponseMessage(event, bytesMsg, apisToRedactInLogs)} ${getErrorMessage(
error
)}`;
} else { } else {
queryMsg = getErrorMessage(error); queryMsg = getErrorMessage(error);
} }
} else { } else {
queryMsg = getResponseMessage(event, bytesMsg); queryMsg = getResponseMessage(event, bytesMsg, apisToRedactInLogs);
} }
queryLogger.debug(queryMsg, meta); queryLogger.debug(queryMsg, meta);
@ -137,7 +203,7 @@ export const instrumentEsQueryAndDeprecationLogger = ({
? 'kibana' ? 'kibana'
: 'user'; : 'user';
// Strip the first 5 stack trace lines as these are irrelavent to finding the call site // Strip the first 5 stack trace lines as these are irrelevant to finding the call site
const stackTrace = new Error().stack?.split('\n').slice(5).join('\n'); const stackTrace = new Error().stack?.split('\n').slice(5).join('\n');
deprecationLogger.debug( deprecationLogger.debug(

View file

@ -30,6 +30,7 @@ test('set correct defaults', () => {
expect(configValue).toMatchInlineSnapshot(` expect(configValue).toMatchInlineSnapshot(`
ElasticsearchConfig { ElasticsearchConfig {
"apiVersion": "master", "apiVersion": "master",
"apisToRedactInLogs": Array [],
"compression": false, "compression": false,
"customHeaders": Object {}, "customHeaders": Object {},
"healthCheckDelay": "PT2.5S", "healthCheckDelay": "PT2.5S",

View file

@ -13,7 +13,11 @@ import { Duration } from 'moment';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import type { ServiceConfigDescriptor } from '@kbn/core-base-server-internal'; import type { ServiceConfigDescriptor } from '@kbn/core-base-server-internal';
import type { ConfigDeprecationProvider } from '@kbn/config'; import type { ConfigDeprecationProvider } from '@kbn/config';
import type { IElasticsearchConfig, ElasticsearchSslConfig } from '@kbn/core-elasticsearch-server'; import type {
IElasticsearchConfig,
ElasticsearchSslConfig,
ElasticsearchApiToRedactInLogs,
} from '@kbn/core-elasticsearch-server';
import { getReservedHeaders } from './default_headers'; import { getReservedHeaders } from './default_headers';
const hostURISchema = schema.uri({ scheme: ['http', 'https'] }); const hostURISchema = schema.uri({ scheme: ['http', 'https'] });
@ -169,6 +173,13 @@ export const configSchema = schema.object({
}), }),
schema.boolean({ defaultValue: false }) schema.boolean({ defaultValue: false })
), ),
apisToRedactInLogs: schema.arrayOf(
schema.object({
path: schema.string(),
method: schema.maybe(schema.string()),
}),
{ defaultValue: [] }
),
}); });
const deprecations: ConfigDeprecationProvider = () => [ const deprecations: ConfigDeprecationProvider = () => [
@ -402,6 +413,11 @@ export class ElasticsearchConfig implements IElasticsearchConfig {
*/ */
public readonly customHeaders: ElasticsearchConfigType['customHeaders']; public readonly customHeaders: ElasticsearchConfigType['customHeaders'];
/**
* Extends the list of APIs that should be redacted in logs.
*/
public readonly apisToRedactInLogs: ElasticsearchApiToRedactInLogs[];
constructor(rawConfig: ElasticsearchConfigType) { constructor(rawConfig: ElasticsearchConfigType) {
this.ignoreVersionMismatch = rawConfig.ignoreVersionMismatch; this.ignoreVersionMismatch = rawConfig.ignoreVersionMismatch;
this.apiVersion = rawConfig.apiVersion; this.apiVersion = rawConfig.apiVersion;
@ -425,6 +441,7 @@ export class ElasticsearchConfig implements IElasticsearchConfig {
this.idleSocketTimeout = rawConfig.idleSocketTimeout; this.idleSocketTimeout = rawConfig.idleSocketTimeout;
this.compression = rawConfig.compression; this.compression = rawConfig.compression;
this.skipStartupConnectionCheck = rawConfig.skipStartupConnectionCheck; this.skipStartupConnectionCheck = rawConfig.skipStartupConnectionCheck;
this.apisToRedactInLogs = rawConfig.apisToRedactInLogs;
const { alwaysPresentCertificate, verificationMode } = rawConfig.ssl; const { alwaysPresentCertificate, verificationMode } = rawConfig.ssl;
const { key, keyPassphrase, certificate, certificateAuthorities } = readKeyAndCerts(rawConfig); const { key, keyPassphrase, certificate, certificateAuthorities } = readKeyAndCerts(rawConfig);

View file

@ -22,6 +22,7 @@ export type {
FakeRequest, FakeRequest,
ElasticsearchClientSslConfig, ElasticsearchClientSslConfig,
ElasticsearchClientConfig, ElasticsearchClientConfig,
ElasticsearchApiToRedactInLogs,
} from './src/client'; } from './src/client';
export type { export type {

View file

@ -8,6 +8,23 @@
import type { Duration } from 'moment'; import type { Duration } from 'moment';
/**
* Definition of an API that should redact the requested body in the logs
*/
export interface ElasticsearchApiToRedactInLogs {
/**
* The ES path.
* - If specified as a string, it'll be checked as `contains`.
* - If specified as a RegExp, it'll be tested against the path.
*/
path: string | RegExp;
/**
* HTTP method.
* If not provided, the path will be checked for all methods.
*/
method?: string;
}
/** /**
* Configuration options to be used to create a {@link IClusterClient | cluster client} * Configuration options to be used to create a {@link IClusterClient | cluster client}
* *
@ -32,6 +49,7 @@ export interface ElasticsearchClientConfig {
requestTimeout?: Duration | number; requestTimeout?: Duration | number;
caFingerprint?: string; caFingerprint?: string;
ssl?: ElasticsearchClientSslConfig; ssl?: ElasticsearchClientSslConfig;
apisToRedactInLogs?: ElasticsearchApiToRedactInLogs[];
} }
/** /**

View file

@ -19,4 +19,8 @@ export type {
UnauthorizedErrorHandlerRetryResult, UnauthorizedErrorHandlerRetryResult,
UnauthorizedErrorHandlerNotHandledResult, UnauthorizedErrorHandlerNotHandledResult,
} from './unauthorized_error_handler'; } from './unauthorized_error_handler';
export type { ElasticsearchClientConfig, ElasticsearchClientSslConfig } from './client_config'; export type {
ElasticsearchClientConfig,
ElasticsearchClientSslConfig,
ElasticsearchApiToRedactInLogs,
} from './client_config';

View file

@ -7,6 +7,7 @@
*/ */
import type { Duration } from 'moment'; import type { Duration } from 'moment';
import type { ElasticsearchApiToRedactInLogs } from './client';
/** /**
* @public * @public
@ -139,6 +140,11 @@ export interface IElasticsearchConfig {
* either `certificate` or `full`. * either `certificate` or `full`.
*/ */
readonly ssl: ElasticsearchSslConfig; readonly ssl: ElasticsearchSslConfig;
/**
* Extends the list of APIs that should be redacted in logs.
*/
readonly apisToRedactInLogs: ElasticsearchApiToRedactInLogs[];
} }
/** /**

View file

@ -55,6 +55,7 @@ describe('config schema', () => {
"debug_mode": false, "debug_mode": false,
"elasticsearch": Object { "elasticsearch": Object {
"apiVersion": "master", "apiVersion": "master",
"apisToRedactInLogs": Array [],
"compression": false, "compression": false,
"customHeaders": Object {}, "customHeaders": Object {},
"healthCheck": Object { "healthCheck": Object {