Support for reindexing APM indices (#29845)

Signed-off-by: Tyler Smalley <tyler.smalley@elastic.co>
This commit is contained in:
Tyler Smalley 2019-02-05 19:47:48 -08:00 committed by GitHub
parent 6ad036b187
commit c16849dc3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 2446 additions and 111 deletions

View file

@ -0,0 +1,22 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export interface ApmOssPlugin {
indexPatterns: string[];
}

View file

@ -17,6 +17,8 @@
* under the License.
*/
import _ from 'lodash';
export default function apmOss(kibana) {
return new kibana.Plugin({
id: 'apm_oss',
@ -30,12 +32,24 @@ export default function apmOss(kibana) {
indexPattern: Joi.string().default('apm-*'),
// ES Indices
sourcemapIndices: Joi.string().default('apm-*'),
errorIndices: Joi.string().default('apm-*'),
onboardingIndices: Joi.string().default('apm-*'),
spanIndices: Joi.string().default('apm-*'),
transactionIndices: Joi.string().default('apm-*'),
spanIndices: Joi.string().default('apm-*'),
metricsIndices: Joi.string().default('apm-*'),
onboardingIndices: Joi.string().default('apm-*'),
}).default();
},
init(server) {
server.expose('indexPatterns', _.uniq([
'sourcemapIndices',
'errorIndices',
'transactionIndices',
'spanIndices',
'metricsIndices',
'onboardingIndices'
].map(type => server.config().get(`apm_oss.${type}`))));
}
});
}

View file

@ -202,13 +202,15 @@ export interface DeprecationInfo {
details?: string;
}
export interface IndexSettingsDeprecationInfo {
[indexName: string]: DeprecationInfo[];
}
export interface DeprecationAPIResponse {
cluster_settings: DeprecationInfo[];
ml_settings: DeprecationInfo[];
node_settings: DeprecationInfo[];
index_settings: {
[indexName: string]: DeprecationInfo[];
};
index_settings: IndexSettingsDeprecationInfo;
}
export interface CallClusterOptions {

View file

@ -19,7 +19,9 @@
import { Server } from 'hapi';
import { ApmOssPlugin } from '../legacy/core_plugins/apm_oss';
import { CallClusterWithRequest, ElasticsearchPlugin } from '../legacy/core_plugins/elasticsearch';
import { IndexPatternsServiceFactory } from './index_patterns';
import { SavedObjectsClient, SavedObjectsService } from './saved_objects';
@ -33,6 +35,7 @@ declare module 'hapi' {
elasticsearch: ElasticsearchPlugin;
kibana: any;
spaces: any;
apm_oss: ApmOssPlugin;
// add new plugin types here
}

View file

@ -53,6 +53,7 @@ export enum ReindexWarning {
booleanFields = 1,
// 7.0 -> 8.0 warnings
apmReindex,
}
export enum IndexGroup {

View file

@ -3,8 +3,8 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Server } from 'hapi';
import Joi from 'joi';
import { Legacy } from 'kibana';
import { resolve } from 'path';
import mappings from './mappings.json';
import { initServer } from './server';
@ -35,7 +35,7 @@ export function upgradeAssistant(kibana: any) {
}).default();
},
init(server: Server) {
init(server: Legacy.Server) {
// Add server routes and initialize the plugin here
initServer(server);
},

View file

@ -135,7 +135,7 @@ export class IndexDeprecationTableUI extends React.Component<
// NOTE: this naive implementation assumes all indices in the table are
// should show the reindex button. This should work for known usecases.
const { indices } = this.props;
if (!indices.find(i => i.reindex)) {
if (!indices.find(i => i.reindex === true)) {
return null;
}
@ -143,7 +143,7 @@ export class IndexDeprecationTableUI extends React.Component<
actions: [
{
render(indexDep: IndexDeprecationDetails) {
return <ReindexButton indexName={indexDep.index} />;
return <ReindexButton indexName={indexDep.index!} />;
},
},
],

View file

@ -10,13 +10,10 @@ import { DeprecationInfo } from 'src/legacy/core_plugins/elasticsearch';
import { EnrichedDeprecationInfo } from '../../../../../server/lib/es_migration_apis';
import { GroupByOption } from '../../../types';
import { CURRENT_MAJOR_VERSION } from 'x-pack/plugins/upgrade_assistant/common/version';
import { COLOR_MAP, LEVEL_MAP } from '../constants';
import { DeprecationCell } from './cell';
import { IndexDeprecationDetails, IndexDeprecationTable } from './index_table';
const OLD_INDEX_MESSAGE = `Index created before ${CURRENT_MAJOR_VERSION}.0`;
const sortByLevelDesc = (a: DeprecationInfo, b: DeprecationInfo) => {
return -1 * (LEVEL_MAP[a.level] - LEVEL_MAP[b.level]);
};
@ -37,7 +34,7 @@ const MessageDeprecation: StatelessComponent<{ deprecation: EnrichedDeprecationI
<DeprecationCell
headline={deprecation.message}
healthColor={COLOR_MAP[deprecation.level]}
reindexIndexName={deprecation.message === OLD_INDEX_MESSAGE ? deprecation.index! : undefined}
reindexIndexName={deprecation.reindex ? deprecation.index! : undefined}
docUrl={deprecation.url}
items={items}
/>
@ -91,9 +88,8 @@ export const DeprecationList: StatelessComponent<{
const indices = deprecations.map(dep => ({
index: dep.index!,
details: dep.details,
reindex: dep.message === OLD_INDEX_MESSAGE,
reindex: dep.reindex === true,
}));
return <IndexDeprecation indices={indices} deprecation={deprecations[0]} />;
} else if (currentGroupBy === GroupByOption.index) {
return (

View file

@ -108,8 +108,7 @@ exports[`WarningsFlyoutStep renders 1`] = `
<EuiCode>
1
</EuiCode>
), reindexing converts these fields to
), reindexing converts these fields to
<EuiCode>
true
</EuiCode>
@ -129,6 +128,9 @@ exports[`WarningsFlyoutStep renders 1`] = `
</EuiLink>
</p>
</EuiText>
<EuiSpacer
size="l"
/>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup

View file

@ -79,61 +79,93 @@ export class WarningsFlyoutStep extends React.Component<
<EuiSpacer />
{warnings.includes(ReindexWarning.allField) && (
<EuiText>
<EuiCheckbox
id={idForWarning(ReindexWarning.allField)}
label={
<strong>
<EuiCode>_all</EuiCode> field will be removed
</strong>
}
checked={checkedIds[idForWarning(ReindexWarning.allField)]}
onChange={this.onChange}
/>
<p className="upgWarningsStep__warningDescription">
The <EuiCode>_all</EuiCode> meta field is no longer supported in 7.0. Reindexing
removes the <EuiCode>_all</EuiCode> field in the new index. Ensure that no
application code or scripts reply on this field.
<br />
<EuiLink
href="https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_the_literal__all_literal_meta_field_is_now_disabled_by_default"
target="_blank"
>
Documentation
</EuiLink>
</p>
</EuiText>
<Fragment>
<EuiText>
<EuiCheckbox
id={idForWarning(ReindexWarning.allField)}
label={
<strong>
<EuiCode>_all</EuiCode> field will be removed
</strong>
}
checked={checkedIds[idForWarning(ReindexWarning.allField)]}
onChange={this.onChange}
/>
<p className="upgWarningsStep__warningDescription">
The <EuiCode>_all</EuiCode> meta field is no longer supported in 7.0. Reindexing
removes the <EuiCode>_all</EuiCode> field in the new index. Ensure that no
application code or scripts reply on this field.
<br />
<EuiLink
href="https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_the_literal__all_literal_meta_field_is_now_disabled_by_default"
target="_blank"
>
Documentation
</EuiLink>
</p>
</EuiText>
<EuiSpacer />
</Fragment>
)}
<EuiSpacer />
{warnings.includes(ReindexWarning.apmReindex) && (
<Fragment>
<EuiText>
<EuiCheckbox
id={idForWarning(ReindexWarning.apmReindex)}
label={<strong>This index will be converted to ECS format</strong>}
checked={checkedIds[idForWarning(ReindexWarning.apmReindex)]}
onChange={this.onChange}
/>
<p className="upgWarningsStep__warningDescription">
Starting in version 7.0.0, APM data will be represented in the Elastic Common
Schema. Historical APM data will not visible until it's reindexed.
<br />
<EuiLink
href="https://www.elastic.co/guide/en/apm/get-started/master/apm-release-notes.html"
target="_blank"
>
Documentation
</EuiLink>
</p>
</EuiText>
<EuiSpacer />
</Fragment>
)}
{warnings.includes(ReindexWarning.booleanFields) && (
<EuiText>
<EuiCheckbox
id={idForWarning(ReindexWarning.booleanFields)}
label={
<strong>
Boolean data in <EuiCode>_source</EuiCode> might change
</strong>
}
checked={checkedIds[idForWarning(ReindexWarning.booleanFields)]}
onChange={this.onChange}
/>
<p className="upgWarningsStep__warningDescription">
If a documents contain a boolean field that is neither <EuiCode>true</EuiCode> or{' '}
<EuiCode>false</EuiCode> (for example, <EuiCode>"yes"</EuiCode>,{' '}
<EuiCode>"on"</EuiCode>, <EuiCode>1</EuiCode>), reindexing converts these fields to{' '}
<EuiCode>true</EuiCode> or <EuiCode>false</EuiCode>. Ensure that no application code
or scripts rely on boolean fields in the deprecated format.
<br />
<EuiLink
href="https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields"
target="_blank"
>
Documentation
</EuiLink>
</p>
</EuiText>
<Fragment>
<EuiText>
<EuiCheckbox
id={idForWarning(ReindexWarning.booleanFields)}
label={
<strong>
Boolean data in <EuiCode>_source</EuiCode> might change
</strong>
}
checked={checkedIds[idForWarning(ReindexWarning.booleanFields)]}
onChange={this.onChange}
/>
<p className="upgWarningsStep__warningDescription">
If a documents contain a boolean field that is neither <EuiCode>true</EuiCode> or{' '}
<EuiCode>false</EuiCode> (for example, <EuiCode>"yes"</EuiCode>,{' '}
<EuiCode>"on"</EuiCode>, <EuiCode>1</EuiCode>), reindexing converts these fields
to <EuiCode>true</EuiCode> or <EuiCode>false</EuiCode>. Ensure that no application
code or scripts rely on boolean fields in the deprecated format.
<br />
<EuiLink
href="https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields"
target="_blank"
>
Documentation
</EuiLink>
</p>
</EuiText>
<EuiSpacer />
</Fragment>
)}
</EuiFlyoutBody>
<EuiFlyoutFooter>

View file

@ -34,6 +34,7 @@ Object {
"index": ".monitoring-es-6-2018.11.07",
"level": "warning",
"message": "Coercion of boolean fields",
"reindex": false,
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields",
},
Object {
@ -41,6 +42,7 @@ Object {
"index": "twitter",
"level": "warning",
"message": "Coercion of boolean fields",
"reindex": false,
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields",
},
Object {
@ -48,6 +50,7 @@ Object {
"index": ".kibana",
"level": "warning",
"message": "Coercion of boolean fields",
"reindex": false,
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields",
},
Object {
@ -55,6 +58,7 @@ Object {
"index": ".watcher-history-6-2018.11.07",
"level": "warning",
"message": "Coercion of boolean fields",
"reindex": false,
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields",
},
Object {
@ -62,6 +66,7 @@ Object {
"index": ".monitoring-kibana-6-2018.11.07",
"level": "warning",
"message": "Coercion of boolean fields",
"reindex": false,
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields",
},
Object {
@ -69,6 +74,7 @@ Object {
"index": "twitter2",
"level": "warning",
"message": "Coercion of boolean fields",
"reindex": false,
"url": "https://www.elastic.co/guide/en/elasticsearch/reference/6.0/breaking_60_mappings_changes.html#_coercion_of_boolean_fields",
},
],

View file

@ -0,0 +1,117 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { getDeprecatedApmIndices, isLegacyApmIndex } from './';
function mockedCallWithRequest() {
return jest.fn().mockImplementation(async () => {
return {
'foo-1': {
mappings: {},
},
'foo-2': {
mappings: {
_meta: {
version: '6.7.0',
},
},
},
'foo-3': {
mappings: {
_meta: {
version: '7.0.0',
},
},
},
'foo-4': {
mappings: {
_meta: {
version: '7.1.0',
},
},
},
};
});
}
describe('getDeprecatedApmIndices', () => {
it('calls indices.getMapping', async () => {
const callWithRequest = mockedCallWithRequest();
await getDeprecatedApmIndices(callWithRequest, {} as any, ['foo-*', 'bar-*']);
expect(callWithRequest).toHaveBeenCalledWith({}, 'indices.getMapping', {
index: 'foo-*,bar-*',
filterPath: '*.mappings._meta.version,*.mappings.properties.@timestamp',
});
});
it('includes mappings not yet at 7.0.0', async () => {
const callWithRequest = mockedCallWithRequest();
const deprecations = await getDeprecatedApmIndices(callWithRequest, {} as any, ['foo-*']);
expect(deprecations).toHaveLength(2);
expect(deprecations[0].index).toEqual('foo-1');
expect(deprecations[1].index).toEqual('foo-2');
});
it('formats the deprecations', async () => {
const callWithRequest = mockedCallWithRequest();
// @ts-ignore
const [deprecation, _] = await getDeprecatedApmIndices(callWithRequest, {} as any, ['foo-*']);
expect(deprecation.level).toEqual('warning');
expect(deprecation.message).toEqual('APM index needs converted to 7.x format');
expect(deprecation.url).toEqual(
'https://www.elastic.co/guide/en/apm/get-started/master/apm-release-notes.html'
);
expect(deprecation.details).toEqual('This index was created prior to 7.0');
expect(deprecation.reindex).toBe(true);
});
});
describe('isLegacyApmIndex', () => {
it('is true when for no version', () => {
expect(isLegacyApmIndex('foo-1', ['foo-*'], {})).toEqual(true);
});
it('is true when version is less than 7.0.0', () => {
expect(
isLegacyApmIndex('foo-1', ['foo-*'], {
_meta: { version: '6.7.0' },
})
).toEqual(true);
});
it('is false when version is 7.0.0', () => {
expect(
isLegacyApmIndex('foo-1', ['foo-*'], {
_meta: { version: '7.0.0' },
})
).toEqual(false);
});
it('is false when version is greater than 7.0.0', () => {
expect(
isLegacyApmIndex('foo-1', ['foo-*'], {
_meta: { version: '7.1.0' },
})
).toEqual(false);
});
it('handles multiple index patterns', () => {
expect(
isLegacyApmIndex('bar-1', ['foo-*', 'bar-*'], {
_meta: { version: '6.7.0' },
})
).toEqual(true);
expect(
isLegacyApmIndex('bar-1', ['foo-*', 'bar-*'], {
_meta: { version: '7.0.0' },
})
).toEqual(false);
});
});

View file

@ -0,0 +1,362 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Request } from 'hapi';
import { get } from 'lodash';
import minimatch from 'minimatch';
import semver from 'semver';
import { CallClusterWithRequest } from 'src/legacy/core_plugins/elasticsearch';
import pkg from '../../../../../package.json';
import { EnrichedDeprecationInfo } from '../es_migration_apis';
import { FlatSettings } from '../reindexing/types';
export async function getDeprecatedApmIndices(
callWithRequest: CallClusterWithRequest,
request: Request,
indexPatterns: string[] = []
): Promise<EnrichedDeprecationInfo[]> {
const indices = await callWithRequest(request, 'indices.getMapping', {
index: indexPatterns.join(','),
// we include @timestamp to prevent filtering mappings without a version
// since @timestamp is expected to always exist
filterPath: '*.mappings._meta.version,*.mappings.properties.@timestamp',
});
return Object.keys(indices).reduce((deprecations: EnrichedDeprecationInfo[], index) => {
if (semver.lt(get(indices[index], 'mappings._meta.version', '0.0.0'), pkg.version)) {
deprecations.push({
level: 'warning',
message: 'APM index needs converted to 7.x format',
url: 'https://www.elastic.co/guide/en/apm/get-started/master/apm-release-notes.html',
details: 'This index was created prior to 7.0',
reindex: true,
index,
});
}
return deprecations;
}, []);
}
export const isLegacyApmIndex = (
indexName: string,
apmIndexPatterns: string[] = [],
mappings: FlatSettings['mappings']
) => {
const clientVersion = get(mappings, '_meta.version', '0.0.0');
const find = apmIndexPatterns.find(pattern => {
return minimatch(indexName, pattern) && semver.lt(clientVersion, pkg.version); // no client version or version < 7.0
});
return Boolean(find);
};
// source: https://github.com/elastic/apm-integration-testing/blob/master/tests/server/test_upgrade.py
export const apmReindexScript = `
// add ecs version
ctx._source.ecs = ['version': '1.0.0-beta2'];
// beat -> observer
def beat = ctx._source.remove("beat");
if (beat != null) {
beat.remove("name");
ctx._source.observer = beat;
ctx._source.observer.type = "apm-server";
}
if (! ctx._source.containsKey("observer")) {
ctx._source.observer = new HashMap();
}
// observer.major_version
ctx._source.observer.version_major = 7;
def listening = ctx._source.remove("listening");
if (listening != null) {
ctx._source.observer.listening = listening;
}
// remove host[.name]
// clarify if we can simply delete this or it will be set somewhere else in 7.0
ctx._source.remove("host");
// docker.container -> container
def docker = ctx._source.remove("docker");
if (docker != null && docker.containsKey("container")) {
ctx._source.container = docker.container;
}
// rip up context
HashMap context = ctx._source.remove("context");
if (context != null) {
// context.process -> process
if (context.containsKey("process")) {
ctx._source.process = context.remove("process");
ctx._source.process.args = ctx._source.process.remove("argv");
}
// context.response -> http.response
HashMap resp = context.remove("response");
if (resp != null) {
if (! ctx._source.containsKey("http")) {
ctx._source.http = new HashMap();
}
ctx._source.http.response = resp;
}
// context.request -> http & url
HashMap request = context.remove("request");
if (request != null) {
if (! ctx._source.containsKey("http")) {
ctx._source.http = new HashMap();
}
// context.request.http_version -> http.version
def http_version = request.remove("http_version");
if (http_version != null) {
ctx._source.http.version = http_version;
}
ctx._source.http.request = new HashMap();
// context.request.url -> url
HashMap url = request.remove("url");
def fragment = url.remove("hash");
if (fragment != null) {
url.fragment = fragment;
}
def domain = url.remove("hostname");
if (domain != null) {
url.domain = domain;
}
def path = url.remove("pathname");
if (path != null) {
url.path = path;
}
def scheme = url.remove("protocol");
if (scheme != null) {
def end = scheme.lastIndexOf(":");
if (end > -1) {
scheme = scheme.substring(0, end);
}
url.scheme = scheme
}
def original = url.remove("raw");
if (original != null) {
url.original = original;
}
def port = url.remove("port");
if (port != null) {
try {
int portNum = Integer.parseInt(port);
url.port = portNum;
} catch (Exception e) {
// toss port
}
}
def query = url.remove("search");
if (query != null) {
url.query = query;
}
ctx._source.url = url;
// restore what is left of request, under http
def body = request.remove("body");
ctx._source.http.request = request;
ctx._source.http.request.method = ctx._source.http.request.method?.toLowerCase();
// context.request.body -> http.request.body.original
if (body != null) {
ctx._source.http.request.body = new HashMap();
ctx._source.http.request.body.original = body;
}
}
// context.service.agent -> agent
HashMap service = context.remove("service");
ctx._source.agent = service.remove("agent");
// context.service -> service
ctx._source.service = service;
// context.system -> host
def system = context.remove("system");
if (system != null) {
system.os = new HashMap();
system.os.platform = system.remove("platform");
ctx._source.host = system;
}
// context.tags -> labels
def tags = context.remove("tags");
if (tags != null) {
ctx._source.labels = tags;
}
// context.user -> user & user_agent
if (context.containsKey("user")) {
HashMap user = context.remove("user");
// user.username -> user.name
def username = user.remove("username");
if (username != null) {
user.name = username;
}
// context.user.ip -> client.ip
if (user.containsKey("ip")) {
ctx._source.client = new HashMap();
ctx._source.client.ip = user.remove("ip");
}
def ua = user.remove("user-agent");
if (ua != null) {
ctx._source.user_agent = new HashMap();
// setting original and original.text is not possible in painless
// as original is a keyword in ES template we cannot set it to a HashMap here,
// so the following is the only possible solution:
ctx._source.user_agent.original = ua.substring(0, Integer.min(1024, ua.length()));
}
def pua = user.remove("user_agent");
if (pua != null) {
if (ctx._source.user_agent == null){
ctx._source.user_agent = new HashMap();
}
def os = pua.remove("os");
def osminor = pua.remove("os_minor");
def osmajor = pua.remove("os_major");
def osname = pua.remove("os_name");
if (osminor != null || osmajor != null || osname != null){
ctx._source.user_agent.os = new HashMap();
ctx._source.user_agent.os.full = os;
ctx._source.user_agent.os.version = osmajor + "." + osminor;
ctx._source.user_agent.os.name = osname;
}
def device = pua.remove("device");
if (device != null){
ctx._source.user_agent.device = new HashMap();
ctx._source.user_agent.device.name = device;
}
// not exactly reflecting 7.0, but the closes we can get
def patch = pua.remove("patch");
def minor = pua.remove("minor");
def major = pua.remove("major");
if (patch != null || minor != null || major != null){
ctx._source.user_agent.version = major + "." + minor + "." + patch;
}
}
ctx._source.user = user;
}
// context.custom -> error,transaction,span.custom
def custom = context.remove("custom");
if (custom != null) {
if (ctx._source.processor.event == "span") {
ctx._source.span.custom = custom;
} else if (ctx._source.processor.event == "transaction") {
ctx._source.transaction.custom = custom;
} else if (ctx._source.processor.event == "error") {
ctx._source.error.custom = custom;
}
}
// context.db -> span.db
def db = context.remove("db");
if (db != null) {
ctx._source.span.db = db;
}
// context.http -> span.http
def http = context.remove("http");
if (http != null) {
// context.http.url -> span.http.url.original
def url = http.remove("url");
if (url != null) {
http.url = ["original": url];
}
// context.http.status_code -> span.http.response.status_code
def status_code = http.remove("status_code");
if (status_code != null) {
http.response = ["status_code": status_code];
}
ctx._source.span.http = http;
}
}
if (ctx._source.processor.event == "span") {
// bump timestamp.us by span.start.us for spans
// shouldn't @timestamp this already be a Date?
def ts = ctx._source.get("@timestamp");
if (ts != null && !ctx._source.containsKey("timestamp")) {
// add span.start to @timestamp for rum documents v1
if (ctx._source.context.service.agent.name == "js-base" && ctx._source.span.start.containsKey("us")) {
ts += ctx._source.span.start.us/1000;
}
}
if (ctx._source.span.containsKey("hex_id")) {
ctx._source.span.id = ctx._source.span.remove("hex_id");
}
def parent = ctx._source.span.remove("parent");
if (parent != null && ctx._source.parent == null) {
ctx._source.parent = ["id": parent];
}
}
// create trace.id
if (ctx._source.processor.event == "transaction" || ctx._source.processor.event == "span" || ctx._source.processor.event == "error") {
if (ctx._source.containsKey("transaction")) {
def tr_id = ctx._source.transaction.get("id");
if (ctx._source.trace == null && tr_id != null) {
// create a trace id from the transaction.id
// v1 transaction.id was a UUID, should have 122 random bits or so
ctx._source.trace = new HashMap();
ctx._source.trace.id = tr_id.replace("-", "");
}
}
// create timestamp.us from @timestamp
def ts = ctx._source.get("@timestamp");
if (ts != null && !ctx._source.containsKey("timestamp")) {
//set timestamp.microseconds to @timestamp
ctx._source.timestamp = new HashMap();
ctx._source.timestamp.us = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse(ts).getTime()*1000;
}
}
// transaction.span_count.dropped.total -> transaction.span_count.dropped
if (ctx._source.processor.event == "transaction") {
// transaction.span_count.dropped.total -> transaction.span_count.dropped
if (ctx._source.transaction.containsKey("span_count")) {
def dropped = ctx._source.transaction.span_count.remove("dropped");
if (dropped != null) {
ctx._source.transaction.span_count.dropped = dropped.total;
}
}
}
if (ctx._source.processor.event == "error") {
// culprit is now a keyword, so trim it down to 1024 chars
def culprit = ctx._source.error.remove("culprit");
if (culprit != null) {
ctx._source.error.culprit = culprit.substring(0, Integer.min(1024, culprit.length()));
}
// error.exception is now a list (exception chain)
def exception = ctx._source.error.remove("exception");
if (exception != null) {
ctx._source.error.exception = [exception];
}
}
`;

File diff suppressed because it is too large Load diff

View file

@ -16,6 +16,8 @@ describe('getUpgradeAssistantStatus', () => {
const callWithRequest = jest.fn().mockImplementation(async (req, api, { path }) => {
if (path === '/_migration/deprecations') {
return deprecationsResponse;
} else if (api === 'indices.getMapping') {
return {};
} else {
throw new Error(`Unexpected API call: ${path}`);
}
@ -26,7 +28,7 @@ describe('getUpgradeAssistantStatus', () => {
});
it('calls /_migration/deprecations', async () => {
await getUpgradeAssistantStatus(callWithRequest, {} as any, false);
await getUpgradeAssistantStatus(callWithRequest, {} as any, false, []);
expect(callWithRequest).toHaveBeenCalledWith({}, 'transport.request', {
path: '/_migration/deprecations',
method: 'GET',
@ -34,7 +36,7 @@ describe('getUpgradeAssistantStatus', () => {
});
it('returns the correct shape of data', async () => {
const resp = await getUpgradeAssistantStatus(callWithRequest, {} as any, false);
const resp = await getUpgradeAssistantStatus(callWithRequest, {} as any, false, []);
expect(resp).toMatchSnapshot();
});
@ -47,7 +49,7 @@ describe('getUpgradeAssistantStatus', () => {
};
await expect(
getUpgradeAssistantStatus(callWithRequest, {} as any, false)
getUpgradeAssistantStatus(callWithRequest, {} as any, false, [])
).resolves.toHaveProperty('readyForUpgrade', false);
});
@ -60,7 +62,7 @@ describe('getUpgradeAssistantStatus', () => {
};
await expect(
getUpgradeAssistantStatus(callWithRequest, {} as any, false)
getUpgradeAssistantStatus(callWithRequest, {} as any, false, [])
).resolves.toHaveProperty('readyForUpgrade', true);
});
@ -78,7 +80,7 @@ describe('getUpgradeAssistantStatus', () => {
index_settings: {},
};
const result = await getUpgradeAssistantStatus(callWithRequest, {} as any, true);
const result = await getUpgradeAssistantStatus(callWithRequest, {} as any, true, []);
expect(result).toHaveProperty('readyForUpgrade', true);
expect(result).toHaveProperty('cluster', []);

View file

@ -13,9 +13,12 @@ import {
DeprecationInfo,
} from 'src/legacy/core_plugins/elasticsearch';
import { getDeprecatedApmIndices } from './apm';
export interface EnrichedDeprecationInfo extends DeprecationInfo {
index?: string;
node?: string;
reindex?: boolean;
}
export interface UpgradeAssistantStatus {
@ -27,15 +30,19 @@ export interface UpgradeAssistantStatus {
export async function getUpgradeAssistantStatus(
callWithRequest: CallClusterWithRequest,
req: Request,
isCloudEnabled: boolean
isCloudEnabled: boolean,
apmIndices: string[]
): Promise<UpgradeAssistantStatus> {
const deprecations = await callWithRequest(req, 'transport.request', {
path: '/_migration/deprecations',
method: 'GET',
});
const [deprecations, apmIndexDeprecations] = await Promise.all([
(await callWithRequest(req, 'transport.request', {
path: '/_migration/deprecations',
method: 'GET',
})) as DeprecationAPIResponse,
getDeprecatedApmIndices(callWithRequest, req, apmIndices),
]);
const cluster = getClusterDeprecations(deprecations, isCloudEnabled);
const indices = getCombinedIndexInfos(deprecations);
const indices = getCombinedIndexInfos(deprecations, apmIndexDeprecations);
const criticalWarnings = cluster.concat(indices).filter(d => d.level === 'critical');
@ -47,17 +54,35 @@ export async function getUpgradeAssistantStatus(
}
// Reformats the index deprecations to an array of deprecation warnings extended with an index field.
const getCombinedIndexInfos = (deprecations: DeprecationAPIResponse) =>
Object.keys(deprecations.index_settings).reduce(
(indexDeprecations, indexName) => {
return indexDeprecations.concat(
deprecations.index_settings[indexName].map(
d => ({ ...d, index: indexName } as EnrichedDeprecationInfo)
)
);
},
[] as EnrichedDeprecationInfo[]
);
const getCombinedIndexInfos = (
deprecations: DeprecationAPIResponse,
apmIndexDeprecations: EnrichedDeprecationInfo[]
) => {
const apmIndices = apmIndexDeprecations.reduce((acc, dep) => acc.add(dep.index), new Set());
return Object.keys(deprecations.index_settings)
.reduce(
(indexDeprecations, indexName) => {
// prevent APM indices from showing up for general re-indexing
if (apmIndices.has(indexName)) {
return indexDeprecations;
}
return indexDeprecations.concat(
deprecations.index_settings[indexName].map(
d =>
({
...d,
index: indexName,
reindex: /Index created before/.test(d.message),
} as EnrichedDeprecationInfo)
)
);
},
[] as EnrichedDeprecationInfo[]
)
.concat(apmIndexDeprecations);
};
const getClusterDeprecations = (deprecations: DeprecationAPIResponse, isCloudEnabled: boolean) => {
const combined = deprecations.cluster_settings

View file

@ -10,6 +10,7 @@ import {
PREV_MAJOR_VERSION,
} from 'x-pack/plugins/upgrade_assistant/common/version';
import { ReindexWarning } from '../../../common/types';
import { isLegacyApmIndex } from '../apm';
import { FlatSettings } from './types';
export interface ParsedIndexName {
@ -59,10 +60,16 @@ export const parseIndexName = (indexName: string): ParsedIndexName => {
* Returns an array of warnings that should be displayed to user before reindexing begins.
* @param flatSettings
*/
export const getReindexWarnings = (flatSettings: FlatSettings): ReindexWarning[] => {
const warnings = [
// No warnings yet for 7.0 -> 8.0
] as Array<[ReindexWarning, boolean]>;
export const getReindexWarnings = (
flatSettings: FlatSettings,
apmIndexPatterns: string[] = []
): ReindexWarning[] => {
const indexName = flatSettings.settings['index.provided_name'];
const apmReindexWarning = isLegacyApmIndex(indexName, apmIndexPatterns, flatSettings.mappings);
const warnings = [[ReindexWarning.apmReindex, apmReindexWarning]] as Array<
[ReindexWarning, boolean]
>;
return warnings.filter(([_, applies]) => applies).map(([warning, _]) => warning);
};

View file

@ -16,6 +16,8 @@ import {
ReindexStatus,
ReindexStep,
} from '../../../common/types';
import { apmReindexScript } from '../apm';
import apmMappings from '../apm/mapping.json';
import { ReindexService, reindexServiceFactory } from './reindex_service';
describe('reindexService', () => {
@ -58,7 +60,7 @@ describe('reindexService', () => {
},
})),
};
service = reindexServiceFactory(callCluster, xpackInfo as any, actions);
service = reindexServiceFactory(callCluster, xpackInfo as any, actions, ['apm-*']);
});
describe('hasRequiredPrivileges', () => {
@ -179,14 +181,17 @@ describe('reindexService', () => {
describe('detectReindexWarnings', () => {
it('fetches reindex warnings from flat settings', async () => {
const indexName = 'myIndex';
actions.getFlatSettings.mockResolvedValueOnce({
settings: {},
settings: {
'index.provided_name': indexName,
},
mappings: {
properties: { https: { type: 'boolean' } },
},
});
const reindexWarnings = await service.detectReindexWarnings('myIndex');
const reindexWarnings = await service.detectReindexWarnings(indexName);
expect(reindexWarnings).toEqual([]);
});
@ -727,6 +732,43 @@ describe('reindexService', () => {
});
});
it('used APM mapping for legacy APM index', async () => {
const indexName = 'apm-1';
const newIndexName = 'apm-1-reindexed';
actions.getFlatSettings.mockResolvedValueOnce({
settings: {
'index.number_of_replicas': 5,
},
mappings: {
_meta: {
version: '6.7.0',
},
},
});
callCluster.mockResolvedValueOnce({ acknowledged: true }); // indices.create
await service.processNextStep({
id: '1',
attributes: {
...defaultAttributes,
indexName,
newIndexName,
lastCompletedStep: ReindexStep.readonly,
},
} as ReindexSavedObject);
expect(callCluster).toHaveBeenCalledWith('indices.create', {
index: newIndexName,
body: {
mappings: apmMappings,
settings: {
'index.number_of_replicas': 5,
},
},
});
});
it('fails if create index is not acknowledged', async () => {
callCluster
.mockResolvedValueOnce({ myIndex: settingsMappings })
@ -761,6 +803,13 @@ describe('reindexService', () => {
attributes: { ...defaultAttributes, lastCompletedStep: ReindexStep.newIndexCreated },
} as ReindexSavedObject;
beforeEach(() => {
actions.getFlatSettings.mockResolvedValueOnce({
settings: {},
mappings: {},
});
});
it('starts reindex, saves taskId, and updates lastCompletedStep', async () => {
callCluster.mockResolvedValueOnce({ task: 'xyz' }); // reindex
const updatedOp = await service.processNextStep(reindexOp);
@ -777,6 +826,43 @@ describe('reindexService', () => {
});
});
it('uses APM script for legacy APM index', async () => {
const indexName = 'apm-1';
const newIndexName = 'apm-1-reindexed';
callCluster.mockResolvedValueOnce({ task: 'xyz' }); // reindex
actions.getFlatSettings.mockResolvedValueOnce({
settings: {},
mappings: {
_meta: {
version: '6.7.0',
},
},
});
await service.processNextStep({
id: '1',
attributes: {
...defaultAttributes,
indexName,
newIndexName,
lastCompletedStep: ReindexStep.newIndexCreated,
},
} as ReindexSavedObject);
expect(callCluster).toHaveBeenLastCalledWith('reindex', {
refresh: true,
waitForCompletion: false,
body: {
source: { index: indexName },
dest: { index: newIndexName },
script: {
lang: 'painless',
source: apmReindexScript,
},
},
});
});
it('fails if starting reindex fails', async () => {
callCluster.mockRejectedValueOnce(new Error('blah!')).mockResolvedValueOnce({});
const updatedOp = await service.processNextStep(reindexOp);

View file

@ -15,6 +15,8 @@ import {
ReindexStep,
ReindexWarning,
} from '../../../common/types';
import { apmReindexScript, isLegacyApmIndex } from '../apm';
import apmMappings from '../apm/mapping.json';
import { getReindexWarnings, parseIndexName, transformFlatSettings } from './index_settings';
import { ReindexActions } from './reindex_actions';
@ -91,7 +93,8 @@ export interface ReindexService {
export const reindexServiceFactory = (
callCluster: CallCluster,
xpackInfo: XPackInfo,
actions: ReindexActions
actions: ReindexActions,
apmIndexPatterns: string[] = []
): ReindexService => {
// ------ Utility functions
@ -279,11 +282,13 @@ export const reindexServiceFactory = (
}
const { settings, mappings } = transformFlatSettings(flatSettings);
const legacyApmIndex = isLegacyApmIndex(indexName, apmIndexPatterns, flatSettings.mappings);
const createIndex = await callCluster('indices.create', {
index: newIndexName,
body: {
settings,
mappings,
mappings: legacyApmIndex ? apmMappings : mappings,
},
});
@ -302,13 +307,29 @@ export const reindexServiceFactory = (
*/
const startReindexing = async (reindexOp: ReindexSavedObject) => {
const { indexName } = reindexOp.attributes;
const reindexBody = {
source: { index: indexName },
dest: { index: reindexOp.attributes.newIndexName },
} as any;
const flatSettings = await actions.getFlatSettings(indexName);
if (!flatSettings) {
throw Boom.notFound(`Index ${indexName} does not exist.`);
}
const legacyApmIndex = isLegacyApmIndex(indexName, apmIndexPatterns, flatSettings.mappings);
if (legacyApmIndex) {
reindexBody.script = {
lang: 'painless',
source: apmReindexScript,
};
}
const startReindex = (await callCluster('reindex', {
refresh: true,
waitForCompletion: false,
body: {
source: { index: indexName },
dest: { index: reindexOp.attributes.newIndexName },
},
body: reindexBody,
})) as any;
return actions.updateReindexOp(reindexOp, {
@ -486,7 +507,7 @@ export const reindexServiceFactory = (
if (!flatSettings) {
return null;
} else {
return getReindexWarnings(flatSettings);
return getReindexWarnings(flatSettings, apmIndexPatterns);
}
},

View file

@ -13,11 +13,16 @@ export interface MappingProperties {
[key: string]: Mapping;
}
interface MetaProperties {
[key: string]: string;
}
export interface FlatSettings {
settings: {
[key: string]: string;
};
mappings: {
properties?: MappingProperties;
_meta?: MetaProperties;
};
}

View file

@ -48,16 +48,20 @@ export class ReindexWorker {
private callWithRequest: CallClusterWithRequest,
private callWithInternalUser: CallCluster,
private xpackInfo: XPackInfo,
private readonly log: Server['log']
private readonly log: Server['log'],
private apmIndexPatterns: string[]
) {
if (ReindexWorker.workerSingleton) {
throw new Error(`More than one ReindexWorker cannot be created.`);
}
this.apmIndexPatterns = apmIndexPatterns;
this.reindexService = reindexServiceFactory(
this.callWithInternalUser,
this.xpackInfo,
reindexActionsFactory(this.client, this.callWithInternalUser)
reindexActionsFactory(this.client, this.callWithInternalUser),
apmIndexPatterns
);
ReindexWorker.workerSingleton = this;
@ -161,7 +165,12 @@ export class ReindexWorker {
const fakeRequest = { headers: credential } as Request;
const callCluster = this.callWithRequest.bind(null, fakeRequest) as CallCluster;
const actions = reindexActionsFactory(this.client, callCluster);
const service = reindexServiceFactory(callCluster, this.xpackInfo, actions);
const service = reindexServiceFactory(
callCluster,
this.xpackInfo,
actions,
this.apmIndexPatterns
);
reindexOp = await swallowExceptions(service.processNextStep, this.log)(reindexOp);
// Update credential store with most recent state.

View file

@ -23,6 +23,9 @@ describe('cluster checkup API', () => {
elasticsearch: {
getCluster: () => ({ callWithRequest: jest.fn() } as any),
} as any,
apm_oss: {
indexPatterns: ['apm-*'],
},
cloud: {
isCloudEnabled: false,
},

View file

@ -18,7 +18,14 @@ export function registerClusterCheckupRoutes(server: Legacy.Server) {
method: 'GET',
async handler(request) {
try {
return await getUpgradeAssistantStatus(callWithRequest, request, isCloudEnabled);
const apmIndexPatterns = server.plugins.apm_oss.indexPatterns;
return await getUpgradeAssistantStatus(
callWithRequest,
request,
isCloudEnabled,
apmIndexPatterns
);
} catch (e) {
if (e.status === 403) {
return Boom.forbidden(e.message);

View file

@ -42,6 +42,9 @@ describe('reindex API', () => {
xpack_main: {
info: {},
},
apm_oss: {
indexPatterns: ['apm-*'],
},
} as any;
server.config = () => ({ get: () => '' } as any);
server.decorate('request', 'getSavedObjectsClient', () => jest.fn());

View file

@ -40,7 +40,8 @@ export function registerReindexWorker(server: Server, credentialStore: Credentia
callWithRequest,
callWithInternalUser,
xpackInfo,
log
log,
server.plugins.apm_oss.indexPatterns
);
// Wait for ES connection before starting the polling loop.
@ -59,6 +60,7 @@ export function registerReindexIndicesRoutes(
) {
const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin');
const xpackInfo = server.plugins.xpack_main.info;
const apmIndexPatterns = server.plugins.apm_oss.indexPatterns;
const BASE_PATH = '/api/upgrade_assistant/reindex';
// Start reindex for an index
@ -70,7 +72,12 @@ export function registerReindexIndicesRoutes(
const { indexName } = request.params;
const callCluster = callWithRequest.bind(null, request) as CallCluster;
const reindexActions = reindexActionsFactory(client, callCluster);
const reindexService = reindexServiceFactory(callCluster, xpackInfo, reindexActions);
const reindexService = reindexServiceFactory(
callCluster,
xpackInfo,
reindexActions,
apmIndexPatterns
);
try {
if (!(await reindexService.hasRequiredPrivileges(indexName))) {
@ -111,7 +118,12 @@ export function registerReindexIndicesRoutes(
const { indexName } = request.params;
const callCluster = callWithRequest.bind(null, request) as CallCluster;
const reindexActions = reindexActionsFactory(client, callCluster);
const reindexService = reindexServiceFactory(callCluster, xpackInfo, reindexActions);
const reindexService = reindexServiceFactory(
callCluster,
xpackInfo,
reindexActions,
apmIndexPatterns
);
try {
const hasRequiredPrivileges = await reindexService.hasRequiredPrivileges(indexName);