diff --git a/.buildkite/scripts/steps/artifacts/cloud.sh b/.buildkite/scripts/steps/artifacts/cloud.sh index 8fa04a5d176b0..d9e78c700e972 100644 --- a/.buildkite/scripts/steps/artifacts/cloud.sh +++ b/.buildkite/scripts/steps/artifacts/cloud.sh @@ -43,39 +43,39 @@ jq ' ' .buildkite/scripts/steps/cloud/deploy.json > "$DEPLOYMENT_SPEC" ecctl deployment create --track --output json --file "$DEPLOYMENT_SPEC" &> "$LOGS" -CLOUD_DEPLOYMENT_USERNAME=$(jq --slurp '.[]|select(.resources).resources[] | select(.credentials).credentials.username' "$LOGS") -CLOUD_DEPLOYMENT_PASSWORD=$(jq --slurp '.[]|select(.resources).resources[] | select(.credentials).credentials.password' "$LOGS") +CLOUD_DEPLOYMENT_USERNAME=$(jq -r --slurp '.[]|select(.resources).resources[] | select(.credentials).credentials.username' "$LOGS") +CLOUD_DEPLOYMENT_PASSWORD=$(jq -r --slurp '.[]|select(.resources).resources[] | select(.credentials).credentials.password' "$LOGS") CLOUD_DEPLOYMENT_ID=$(jq -r --slurp '.[0].id' "$LOGS") CLOUD_DEPLOYMENT_STATUS_MESSAGES=$(jq --slurp '[.[]|select(.resources == null)]' "$LOGS") CLOUD_DEPLOYMENT_KIBANA_URL=$(ecctl deployment show "$CLOUD_DEPLOYMENT_ID" | jq -r '.resources.kibana[0].info.metadata.aliased_url') CLOUD_DEPLOYMENT_ELASTICSEARCH_URL=$(ecctl deployment show "$CLOUD_DEPLOYMENT_ID" | jq -r '.resources.elasticsearch[0].info.metadata.aliased_url') -# NOTE: disabled pending log sanitization -# echo "--- Setup FTR" -# export TEST_KIBANA_PROTOCOL=$(node -e "console.log(new URL('$CLOUD_DEPLOYMENT_KIBANA_URL').protocol)") -# export TEST_KIBANA_HOSTNAME=$(node -e "console.log(new URL('$CLOUD_DEPLOYMENT_KIBANA_URL').hostname)") -# export TEST_KIBANA_PORT=$(node -e "console.log(new URL('$CLOUD_DEPLOYMENT_KIBANA_URL').port)") -# export TEST_KIBANA_USERNAME=$CLOUD_DEPLOYMENT_USERNAME" -# export TEST_KIBANA_PASS=$CLOUD_DEPLOYMENT_PASSWORD" +echo "Kibana: $CLOUD_DEPLOYMENT_KIBANA_URL" +echo "ES: $CLOUD_DEPLOYMENT_ELASTICSEARCH_URL" -# export TEST_ES_PROTOCOL=$(node -e "console.log(new URL('$CLOUD_DEPLOYMENT_KIBANA_URL').protocol)") -# export TEST_ES_HOSTNAME==$(node -e "console.log(new URL('$CLOUD_DEPLOYMENT_KIBANA_URL').hostname)") -# export TEST_ES_PORT=$(node -e "console.log(new URL('$CLOUD_DEPLOYMENT_KIBANA_URL').port)") -# export TEST_ES_USER="$CLOUD_DEPLOYMENT_USERNAME" -# export TEST_ES_PASS="$CLOUD_DEPLOYMENT_PASSWORD" +function shutdown { + echo "--- Shutdown deployment" + ecctl deployment shutdown "$CLOUD_DEPLOYMENT_ID" --force --track --output json &> "$LOGS" +} +trap "shutdown" EXIT -# export TEST_BROWSER_HEADLESS=1 +export TEST_KIBANA_PROTOCOL=$(node -e "console.log(new URL('$CLOUD_DEPLOYMENT_KIBANA_URL').protocol.replace(':', ''))") +export TEST_KIBANA_HOSTNAME=$(node -e "console.log(new URL('$CLOUD_DEPLOYMENT_KIBANA_URL').hostname)") +export TEST_KIBANA_PORT=$(node -e "console.log(new URL('$CLOUD_DEPLOYMENT_KIBANA_URL').port)") +export TEST_KIBANA_USERNAME="$CLOUD_DEPLOYMENT_USERNAME" +export TEST_KIBANA_PASSWORD="$CLOUD_DEPLOYMENT_PASSWORD" -# Error: attempted to use the "es" service to fetch Elasticsearch version info but the request failed: ConnectionError: self signed certificate in certificate chain -# export NODE_TLS_REJECT_UNAUTHORIZED=0 +export TEST_ES_PROTOCOL=$(node -e "console.log(new URL('$CLOUD_DEPLOYMENT_ELASTICSEARCH_URL').protocol.replace(':', ''))") +export TEST_ES_HOSTNAME=$(node -e "console.log(new URL('$CLOUD_DEPLOYMENT_ELASTICSEARCH_URL').hostname)") +export TEST_ES_PORT=$(node -e "console.log(new URL('$CLOUD_DEPLOYMENT_ELASTICSEARCH_URL').port)") +export TEST_ES_USERNAME="$CLOUD_DEPLOYMENT_USERNAME" +export TEST_ES_PASSWORD="$CLOUD_DEPLOYMENT_PASSWORD" -# echo "--- Run default functional tests" -# node --no-warnings scripts/functional_test_runner.js --include-tag=cloud -exclude-tag=skipCloud +export TEST_BROWSER_HEADLESS=1 -# echo "--- Run x-pack functional tests" -# cd x-pack -# node --no-warnings scripts/functional_test_runner.js --include-tag=cloud -exclude-tag=skipCloud +# Error: attempted to use the "es" service to fetch Elasticsearch version info but the request failed: ConnectionError: self signed certificate in certificate chain +export NODE_TLS_REJECT_UNAUTHORIZED=0 -echo "--- Shutdown deployment" -ecctl deployment shutdown "$CLOUD_DEPLOYMENT_ID" --force --track --output json &> "$LOGS" +echo "--- FTR - Reporting" +node --no-warnings scripts/functional_test_runner.js --config x-pack/test/functional/apps/visualize/config.ts --include-tag=smoke --quiet diff --git a/.eslintignore b/.eslintignore index 9b745756b6706..bfa69083b1e09 100644 --- a/.eslintignore +++ b/.eslintignore @@ -30,7 +30,7 @@ snapshots.js /x-pack/plugins/reporting/server/export_types/printable_pdf_v2/server/lib/pdf/assets/** # package overrides -/packages/elastic-eslint-config-kibana +/packages/kbn-eslint-config /packages/kbn-plugin-generator/template /packages/kbn-generate/templates /packages/kbn-pm/dist diff --git a/.eslintrc.js b/.eslintrc.js index dfbdd4de96f0a..a921718a97f79 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -230,7 +230,7 @@ const RESTRICTED_IMPORTS = [ module.exports = { root: true, - extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], + extends: ['plugin:@elastic/eui/recommended', '@kbn/eslint-config'], overrides: [ /** @@ -304,7 +304,7 @@ module.exports = { */ { files: [ - 'packages/elastic-eslint-config-kibana/**/*.{js,mjs,ts,tsx}', + 'packages/kbn-eslint-config/**/*.{js,mjs,ts,tsx}', 'packages/kbn-datemath/**/*.{js,mjs,ts,tsx}', ], rules: { diff --git a/docs/api/cases/cases-api-add-comment.asciidoc b/docs/api/cases/cases-api-add-comment.asciidoc index df63cc0ecd65f..203492d6aa632 100644 --- a/docs/api/cases/cases-api-add-comment.asciidoc +++ b/docs/api/cases/cases-api-add-comment.asciidoc @@ -119,6 +119,7 @@ The API returns details about the case and its comments. For example: "syncAlerts":false }, "owner": "cases", + "duration": null, "closed_at": null, "closed_by": null, "created_at": "2022-03-24T00:37:03.906Z", diff --git a/docs/api/cases/cases-api-create.asciidoc b/docs/api/cases/cases-api-create.asciidoc index b7a97fc9cb1b2..73c89937466b3 100644 --- a/docs/api/cases/cases-api-create.asciidoc +++ b/docs/api/cases/cases-api-create.asciidoc @@ -205,6 +205,7 @@ the case identifier, version, and creation time. For example: }, "owner": "securitySolution", "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active", + "duration": null, "closed_at": null, "closed_by": null, "created_at": "2022-05-13T09:16:17.416Z", diff --git a/docs/api/cases/cases-api-find-cases.asciidoc b/docs/api/cases/cases-api-find-cases.asciidoc index abd4e186ff706..3e94dd56ffa36 100644 --- a/docs/api/cases/cases-api-find-cases.asciidoc +++ b/docs/api/cases/cases-api-find-cases.asciidoc @@ -125,6 +125,7 @@ The API returns a JSON object listing the retrieved cases. For example: "syncAlerts": true }, "owner": "securitySolution", + "duration": null, "closed_at": null, "closed_by": null, "created_at": "2022-03-29T13:03:23.533Z", @@ -164,6 +165,7 @@ The API returns a JSON object listing the retrieved cases. For example: "syncAlerts": false }, "owner": "cases", + "duration": null, "closed_at": null, "closed_by": null, "created_at": "2022-03-29T11:30:02.658Z", diff --git a/docs/api/cases/cases-api-get-case.asciidoc b/docs/api/cases/cases-api-get-case.asciidoc index 5abb9ecc1903b..42cf0672065e7 100644 --- a/docs/api/cases/cases-api-get-case.asciidoc +++ b/docs/api/cases/cases-api-get-case.asciidoc @@ -59,6 +59,7 @@ The API returns a JSON object with the retrieved case. For example: "version": "Wzk4LDFd", "comments": [], "totalComment": 0, + "totalAlerts": 0, "closed_at": null, "closed_by": null, "created_at": "2020-03-29T11:30:02.658Z", @@ -90,6 +91,7 @@ The API returns a JSON object with the retrieved case. For example: "syncAlerts": true }, "owner": "securitySolution", + "duration": null, "tags": [ "phishing", "social engineering", diff --git a/docs/api/cases/cases-api-push.asciidoc b/docs/api/cases/cases-api-push.asciidoc index 5b3e4d7c9ef78..16c411104caed 100644 --- a/docs/api/cases/cases-api-push.asciidoc +++ b/docs/api/cases/cases-api-push.asciidoc @@ -68,6 +68,7 @@ The API returns a JSON object representing the pushed case. For example: "syncAlerts": true }, "owner": "securitySolution", + "duration": null, "closed_at": null, "closed_by": null, "created_at": "2022-03-29T11:30:02.658Z", diff --git a/docs/api/cases/cases-api-update-comment.asciidoc b/docs/api/cases/cases-api-update-comment.asciidoc index 020fe403fa7c5..d00d1eb66ea7c 100644 --- a/docs/api/cases/cases-api-update-comment.asciidoc +++ b/docs/api/cases/cases-api-update-comment.asciidoc @@ -106,18 +106,18 @@ The API returns details about the case and its comments. For example: "comments":[{ "id": "8af6ac20-74f6-11ea-b83a-553aecdb28b6", "version": "WzIwNjM3LDFd", - "comment":"That is nothing - Ethan Hunt answered a targeted social media campaign promoting phishy pension schemes to IMF operatives. Even worse, he likes baked beans.", - "type":"user", - "owner":"cases", - "created_at":"2022-03-24T00:37:10.832Z", + "comment": "That is nothing - Ethan Hunt answered a targeted social media campaign promoting phishy pension schemes to IMF operatives. Even worse, he likes baked beans.", + "type": "user", + "owner": "cases", + "created_at": "2022-03-24T00:37:10.832Z", "created_by": { "email": "moneypenny@hms.gov.uk", "full_name": "Ms Moneypenny", "username": "moneypenny" }, - "pushed_at":null, - "pushed_by":null, - "updated_at":"2022-03-24T01:27:06.210Z", + "pushed_at": null, + "pushed_by": null, + "updated_at": "2022-03-24T01:27:06.210Z", "updated_by": { "email": "jbond@hms.gov.uk", "full_name": "James Bond", @@ -125,16 +125,17 @@ The API returns details about the case and its comments. For example: } } ], - "totalAlerts":0, + "totalAlerts": 0, "id": "293f1bc0-74f6-11ea-b83a-553aecdb28b6", "version": "WzIwNjM2LDFd", "totalComment": 1, "title": "This case will self-destruct in 5 seconds", - "tags": ["phishing","social engineering"], + "tags": ["phishing","social engineering"], "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants.", "settings": {"syncAlerts":false}, - "owner": "cases"," - closed_at": null, + "owner": "cases", + "duration": null, + "closed_at": null, "closed_by": null, "created_at": "2022-03-24T00:37:03.906Z", "created_by": { diff --git a/docs/api/cases/cases-api-update.asciidoc b/docs/api/cases/cases-api-update.asciidoc index 7a63d0e8a6a33..ebad2feaedff4 100644 --- a/docs/api/cases/cases-api-update.asciidoc +++ b/docs/api/cases/cases-api-update.asciidoc @@ -226,6 +226,7 @@ The API returns the updated case with a new `version` value. For example: }, "owner": "securitySolution", "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active!", + "duration": null, "closed_at": null, "closed_by": null, "created_at": "2022-05-13T09:16:17.416Z", diff --git a/docs/developer/getting-started/running-kibana-advanced.asciidoc b/docs/developer/getting-started/running-kibana-advanced.asciidoc index 68a4951ea1c21..5b2a58836008c 100644 --- a/docs/developer/getting-started/running-kibana-advanced.asciidoc +++ b/docs/developer/getting-started/running-kibana-advanced.asciidoc @@ -30,7 +30,7 @@ If youโ€™re installing dependencies and seeing an error that looks something like .... -Unsupported URL Type: link:packages/elastic-eslint-config-kibana +Unsupported URL Type: link:packages/kbn-eslint-config .... youโ€™re likely running `npm`. To install dependencies in {kib} you diff --git a/docs/setup/upgrade/resolving-migration-failures.asciidoc b/docs/setup/upgrade/resolving-migration-failures.asciidoc index 7cbc972b102ab..2c3f66f2354dd 100644 --- a/docs/setup/upgrade/resolving-migration-failures.asciidoc +++ b/docs/setup/upgrade/resolving-migration-failures.asciidoc @@ -187,3 +187,10 @@ PUT /_cluster/settings } } -------------------------------------------- + +[float] +[[cluster-shard-limit-exceeded]] +==== {es} cluster shard limit exceeded +When upgrading, {kib} creates new indices requiring a small number of new shards. If the amount of open {es} shards approaches or exceeds the {es} `cluster.max_shards_per_node` setting, {kib} is unable to complete the upgrade. Ensure that {kib} is able to add at least 10 more shards by removing indices to clear up resources, or by increasing the `cluster.max_shards_per_node` setting. + +For more information, refer to the documentation on {ref}/allocation-total-shards.html[total shards per node]. \ No newline at end of file diff --git a/package.json b/package.json index 74e4b3e211504..e018660238259 100644 --- a/package.json +++ b/package.json @@ -101,8 +101,8 @@ "@dnd-kit/core": "^3.1.1", "@dnd-kit/sortable": "^4.0.0", "@dnd-kit/utilities": "^2.0.0", - "@elastic/apm-rum": "^5.11.0", - "@elastic/apm-rum-react": "^1.4.0", + "@elastic/apm-rum": "^5.11.1", + "@elastic/apm-rum-react": "^1.4.1", "@elastic/apm-synthtrace": "link:bazel-bin/packages/elastic-apm-synthtrace", "@elastic/charts": "46.0.1", "@elastic/datemath": "5.0.3", @@ -470,7 +470,6 @@ "@cypress/code-coverage": "^3.9.12", "@cypress/snapshot": "^2.1.7", "@cypress/webpack-preprocessor": "^5.6.0", - "@elastic/eslint-config-kibana": "link:bazel-bin/packages/elastic-eslint-config-kibana", "@elastic/eslint-plugin-eui": "0.0.2", "@elastic/github-checks-reporter": "0.0.20b3", "@elastic/makelogs": "^6.0.0", @@ -493,6 +492,7 @@ "@kbn/docs-utils": "link:bazel-bin/packages/kbn-docs-utils", "@kbn/es": "link:bazel-bin/packages/kbn-es", "@kbn/es-archiver": "link:bazel-bin/packages/kbn-es-archiver", + "@kbn/eslint-config": "link:bazel-bin/packages/kbn-eslint-config", "@kbn/eslint-plugin-eslint": "link:bazel-bin/packages/kbn-eslint-plugin-eslint", "@kbn/expect": "link:bazel-bin/packages/kbn-expect", "@kbn/find-used-node-modules": "link:bazel-bin/packages/kbn-find-used-node-modules", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 5f435f583a36a..5a06233d0e72d 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -15,7 +15,6 @@ filegroup( "//packages/analytics/shippers/elastic_v3/server:build", "//packages/analytics/shippers/fullstory:build", "//packages/elastic-apm-synthtrace:build", - "//packages/elastic-eslint-config-kibana:build", "//packages/elastic-safer-lodash-set:build", "//packages/kbn-ace:build", "//packages/kbn-alerts:build", @@ -43,6 +42,7 @@ filegroup( "//packages/kbn-es-archiver:build", "//packages/kbn-es-query:build", "//packages/kbn-es:build", + "//packages/kbn-eslint-config:build", "//packages/kbn-eslint-plugin-eslint:build", "//packages/kbn-eslint-plugin-imports:build", "//packages/kbn-expect:build", diff --git a/packages/elastic-apm-synthtrace/src/lib/apm/apm_fields.ts b/packages/elastic-apm-synthtrace/src/lib/apm/apm_fields.ts index f117fc879c0e7..1e972b5663113 100644 --- a/packages/elastic-apm-synthtrace/src/lib/apm/apm_fields.ts +++ b/packages/elastic-apm-synthtrace/src/lib/apm/apm_fields.ts @@ -85,6 +85,10 @@ export type ApmFields = Fields & 'span.destination.service.response_time.count': number; 'span.self_time.count': number; 'span.self_time.sum.us': number; + 'span.links': Array<{ + trace: { id: string }; + span: { id: string }; + }>; 'cloud.provider': string; 'cloud.project.name': string; 'cloud.service.name': string; diff --git a/packages/elastic-apm-synthtrace/src/scripts/examples/04_span_links.ts b/packages/elastic-apm-synthtrace/src/scripts/examples/04_span_links.ts new file mode 100644 index 0000000000000..f73a3b4f4fb49 --- /dev/null +++ b/packages/elastic-apm-synthtrace/src/scripts/examples/04_span_links.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { compact, shuffle } from 'lodash'; +import { apm, ApmFields, EntityArrayIterable, timerange } from '../..'; +import { generateLongId, generateShortId } from '../../lib/utils/generate_id'; +import { Scenario } from '../scenario'; + +function generateExternalSpanLinks() { + // randomly creates external span links 0 - 10 + return Array(Math.floor(Math.random() * 11)) + .fill(0) + .map(() => ({ span: { id: generateLongId() }, trace: { id: generateShortId() } })); +} + +function getSpanLinksFromEvents(events: ApmFields[]) { + return compact( + events.map((event) => { + const spanId = event['span.id']; + return spanId ? { span: { id: spanId }, trace: { id: event['trace.id']! } } : undefined; + }) + ); +} + +const scenario: Scenario = async () => { + return { + generate: ({ from, to }) => { + const producerInternalOnlyInstance = apm + .service('producer-internal-only', 'production', 'go') + .instance('instance-a'); + const producerInternalOnlyEvents = timerange( + new Date('2022-04-25T19:00:00.000Z'), + new Date('2022-04-25T19:01:00.000Z') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return producerInternalOnlyInstance + .transaction('Transaction A') + .timestamp(timestamp) + .duration(1000) + .success() + .children( + producerInternalOnlyInstance + .span('Span A', 'custom') + .timestamp(timestamp + 50) + .duration(100) + .success() + ); + }); + + const producerInternalOnlyApmFields = producerInternalOnlyEvents.toArray(); + const spanASpanLink = getSpanLinksFromEvents(producerInternalOnlyApmFields); + + const producerConsumerInstance = apm + .service('producer-consumer', 'production', 'java') + .instance('instance-b'); + const producerConsumerEvents = timerange(from, to) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return producerConsumerInstance + .transaction('Transaction B') + .timestamp(timestamp) + .duration(1000) + .success() + .children( + producerConsumerInstance + .span('Span B', 'external') + .defaults({ + 'span.links': shuffle([...generateExternalSpanLinks(), ...spanASpanLink]), + }) + .timestamp(timestamp + 50) + .duration(900) + .success() + ); + }); + + const producerConsumerApmFields = producerConsumerEvents.toArray(); + const spanBSpanLink = getSpanLinksFromEvents(producerConsumerApmFields); + + const consumerInstance = apm.service('consumer', 'production', 'ruby').instance('instance-c'); + const consumerEvents = timerange(from, to) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return consumerInstance + .transaction('Transaction C') + .timestamp(timestamp) + .duration(1000) + .success() + .children( + consumerInstance + .span('Span C', 'external') + .defaults({ 'span.links': spanBSpanLink }) + .timestamp(timestamp + 50) + .duration(900) + .success() + ); + }); + + return new EntityArrayIterable(producerInternalOnlyApmFields) + .merge(consumerEvents) + .merge(new EntityArrayIterable(producerConsumerApmFields)); + }, + }; +}; + +export default scenario; diff --git a/packages/elastic-eslint-config-kibana/.npmignore b/packages/elastic-eslint-config-kibana/.npmignore deleted file mode 100644 index 2ba159593147d..0000000000000 --- a/packages/elastic-eslint-config-kibana/.npmignore +++ /dev/null @@ -1,2 +0,0 @@ -.eslintrc.yaml -tasks diff --git a/packages/kbn-babel-preset/styled_components_files.js b/packages/kbn-babel-preset/styled_components_files.js index 53052809b6b2f..1c5cf2af81f0f 100644 --- a/packages/kbn-babel-preset/styled_components_files.js +++ b/packages/kbn-babel-preset/styled_components_files.js @@ -9,7 +9,7 @@ module.exports = { /** * Synchronized regex list of files that use `styled-components`. - * Used by `kbn-babel-preset` and `elastic-eslint-config-kibana`. + * Used by `kbn-babel-preset` and `kbn-eslint-config`. */ USES_STYLED_COMPONENTS: [ /packages[\/\\]kbn-ui-shared-deps-(npm|src)[\/\\]/, diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 9c59db0f47f2b..55909e360b0e5 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -653,6 +653,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { resolveMigrationFailures: `${KIBANA_DOCS}resolve-migrations-failures.html`, repeatedTimeoutRequests: `${KIBANA_DOCS}resolve-migrations-failures.html#_repeated_time_out_requests_that_eventually_fail`, routingAllocationDisabled: `${KIBANA_DOCS}resolve-migrations-failures.html#routing-allocation-disabled`, + clusterShardLimitExceeded: `${KIBANA_DOCS}resolve-migrations-failures.html#cluster-shard-limit-exceeded`, }, }); }; diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index ce6533b93f9e3..c492509e80511 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -408,5 +408,6 @@ export interface DocLinks { readonly resolveMigrationFailures: string; readonly repeatedTimeoutRequests: string; readonly routingAllocationDisabled: string; + readonly clusterShardLimitExceeded: string; }; } diff --git a/packages/elastic-eslint-config-kibana/.eslintrc.js b/packages/kbn-eslint-config/.eslintrc.js similarity index 100% rename from packages/elastic-eslint-config-kibana/.eslintrc.js rename to packages/kbn-eslint-config/.eslintrc.js diff --git a/packages/elastic-eslint-config-kibana/.gitignore b/packages/kbn-eslint-config/.gitignore similarity index 100% rename from packages/elastic-eslint-config-kibana/.gitignore rename to packages/kbn-eslint-config/.gitignore diff --git a/packages/elastic-eslint-config-kibana/BUILD.bazel b/packages/kbn-eslint-config/BUILD.bazel similarity index 88% rename from packages/elastic-eslint-config-kibana/BUILD.bazel rename to packages/kbn-eslint-config/BUILD.bazel index 9dceec268418b..6eb7ff7c723ac 100644 --- a/packages/elastic-eslint-config-kibana/BUILD.bazel +++ b/packages/kbn-eslint-config/BUILD.bazel @@ -1,8 +1,8 @@ load("@build_bazel_rules_nodejs//:index.bzl", "js_library") load("//src/dev/bazel:index.bzl", "pkg_npm") -PKG_BASE_NAME = "elastic-eslint-config-kibana" -PKG_REQUIRE_NAME = "@elastic/eslint-config-kibana" +PKG_BASE_NAME = "kbn-eslint-config" +PKG_REQUIRE_NAME = "@kbn/eslint-config" SOURCE_FILES = glob([ ".eslintrc.js", @@ -22,7 +22,6 @@ filegroup( NPM_MODULE_EXTRA_FILES = [ "package.json", - "README.md", ] RUNTIME_DEPS = [ diff --git a/packages/elastic-eslint-config-kibana/README.md b/packages/kbn-eslint-config/README.md similarity index 68% rename from packages/elastic-eslint-config-kibana/README.md rename to packages/kbn-eslint-config/README.md index 2049440cd8ff7..cca5551a07aba 100644 --- a/packages/elastic-eslint-config-kibana/README.md +++ b/packages/kbn-eslint-config/README.md @@ -10,7 +10,7 @@ in your `.eslintrc`: ```javascript { extends: [ - '@elastic/eslint-config-kibana' + '@kbn/eslint-config' ] } ``` @@ -18,14 +18,14 @@ in your `.eslintrc`: ## Optional jest config If the project uses the [jest test runner](https://facebook.github.io/jest/), -the `@elastic/eslint-config-kibana/jest` config can be extended as well to use +the `@kbn/eslint-config/jest` config can be extended as well to use `eslint-plugin-jest` and add settings specific to it: ```javascript { extends: [ - '@elastic/eslint-config-kibana', - '@elastic/eslint-config-kibana/jest' + '@kbn/eslint-config', + '@kbn/eslint-config/jest' ] } ``` diff --git a/packages/elastic-eslint-config-kibana/javascript.js b/packages/kbn-eslint-config/javascript.js similarity index 100% rename from packages/elastic-eslint-config-kibana/javascript.js rename to packages/kbn-eslint-config/javascript.js diff --git a/packages/elastic-eslint-config-kibana/jest.js b/packages/kbn-eslint-config/jest.js similarity index 100% rename from packages/elastic-eslint-config-kibana/jest.js rename to packages/kbn-eslint-config/jest.js diff --git a/packages/elastic-eslint-config-kibana/package.json b/packages/kbn-eslint-config/package.json similarity index 78% rename from packages/elastic-eslint-config-kibana/package.json rename to packages/kbn-eslint-config/package.json index a5007de28584c..eb9f7a4b08246 100644 --- a/packages/elastic-eslint-config-kibana/package.json +++ b/packages/kbn-eslint-config/package.json @@ -1,6 +1,6 @@ { - "name": "@elastic/eslint-config-kibana", - "version": "0.15.0", + "name": "@kbn/eslint-config", + "version": "1.0.0", "description": "The eslint config used by the kibana team", "main": ".eslintrc.js", "repository": { @@ -14,7 +14,7 @@ "author": "Spencer Alger ", "license": "Apache-2.0", "bugs": { - "url": "https://github.com/elastic/kibana/tree/main/packages/elastic-eslint-config-kibana" + "url": "https://github.com/elastic/kibana/tree/main/packages/kbn-eslint-config" }, - "homepage": "https://github.com/elastic/kibana/tree/main/packages/elastic-eslint-config-kibana" + "homepage": "https://github.com/elastic/kibana/tree/main/packages/kbn-eslint-config" } \ No newline at end of file diff --git a/packages/elastic-eslint-config-kibana/react.js b/packages/kbn-eslint-config/react.js similarity index 100% rename from packages/elastic-eslint-config-kibana/react.js rename to packages/kbn-eslint-config/react.js diff --git a/packages/elastic-eslint-config-kibana/restricted_globals.js b/packages/kbn-eslint-config/restricted_globals.js similarity index 100% rename from packages/elastic-eslint-config-kibana/restricted_globals.js rename to packages/kbn-eslint-config/restricted_globals.js diff --git a/packages/elastic-eslint-config-kibana/typescript.js b/packages/kbn-eslint-config/typescript.js similarity index 100% rename from packages/elastic-eslint-config-kibana/typescript.js rename to packages/kbn-eslint-config/typescript.js diff --git a/packages/kbn-plugin-generator/template/.eslintrc.js.ejs b/packages/kbn-plugin-generator/template/.eslintrc.js.ejs index d063fc481b718..5a65d0cfb8e02 100644 --- a/packages/kbn-plugin-generator/template/.eslintrc.js.ejs +++ b/packages/kbn-plugin-generator/template/.eslintrc.js.ejs @@ -1,7 +1,7 @@ module.exports = { root: true, extends: [ - '@elastic/eslint-config-kibana', + '@kbn/eslint-config', 'plugin:@elastic/eui/recommended' ], rules: { diff --git a/packages/kbn-pm/README.md b/packages/kbn-pm/README.md index eb1ac6ffa92aa..33d1fe6590f4b 100644 --- a/packages/kbn-pm/README.md +++ b/packages/kbn-pm/README.md @@ -19,7 +19,7 @@ From a plugin perspective there are two different types of Kibana dependencies: runtime and static dependencies. Runtime dependencies are things that are instantiated at runtime and that are injected into the plugin, for example config and elasticsearch clients. Static dependencies are those dependencies -that we want to `import`. `elastic-eslint-config-kibana` is one example of this, and +that we want to `import`. `kbn-eslint-config` is one example of this, and it's actually needed because eslint requires it to be a separate package. But we also have dependencies like `datemath`, `flot`, `eui` and others that we control, but where we want to `import` them in plugins instead of injecting them diff --git a/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.scss b/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.scss index 91b96641047e8..c21f5e1bbab99 100644 --- a/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.scss +++ b/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.scss @@ -10,6 +10,9 @@ $euiSideNavEmphasizedBackgroundColor: transparentize($euiColorLightShade, .7); @include euiSideNavEmbellish; @include euiYScroll; + display: flex; + flex-direction: column; + @include euiBreakpoint('m' ,'l', 'xl') { width: 248px; padding: $euiSizeL; diff --git a/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap b/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap index e6e1fc2cdc21d..971e2d9129d47 100644 --- a/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap +++ b/src/core/server/saved_objects/migrations/__snapshots__/migrations_state_action_machine.test.ts.snap @@ -33,6 +33,7 @@ Object { ], "maxBatchSizeBytes": 100000000, "migrationDocLinks": Object { + "clusterShardLimitExceeded": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#cluster-shard-limit-exceeded", "repeatedTimeoutRequests": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#_repeated_time_out_requests_that_eventually_fail", "resolveMigrationFailures": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html", "routingAllocationDisabled": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#routing-allocation-disabled", @@ -204,6 +205,7 @@ Object { ], "maxBatchSizeBytes": 100000000, "migrationDocLinks": Object { + "clusterShardLimitExceeded": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#cluster-shard-limit-exceeded", "repeatedTimeoutRequests": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#_repeated_time_out_requests_that_eventually_fail", "resolveMigrationFailures": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html", "routingAllocationDisabled": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#routing-allocation-disabled", @@ -379,6 +381,7 @@ Object { ], "maxBatchSizeBytes": 100000000, "migrationDocLinks": Object { + "clusterShardLimitExceeded": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#cluster-shard-limit-exceeded", "repeatedTimeoutRequests": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#_repeated_time_out_requests_that_eventually_fail", "resolveMigrationFailures": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html", "routingAllocationDisabled": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#routing-allocation-disabled", @@ -558,6 +561,7 @@ Object { ], "maxBatchSizeBytes": 100000000, "migrationDocLinks": Object { + "clusterShardLimitExceeded": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#cluster-shard-limit-exceeded", "repeatedTimeoutRequests": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#_repeated_time_out_requests_that_eventually_fail", "resolveMigrationFailures": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html", "routingAllocationDisabled": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#routing-allocation-disabled", @@ -763,6 +767,7 @@ Object { ], "maxBatchSizeBytes": 100000000, "migrationDocLinks": Object { + "clusterShardLimitExceeded": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#cluster-shard-limit-exceeded", "repeatedTimeoutRequests": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#_repeated_time_out_requests_that_eventually_fail", "resolveMigrationFailures": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html", "routingAllocationDisabled": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#routing-allocation-disabled", @@ -945,6 +950,7 @@ Object { ], "maxBatchSizeBytes": 100000000, "migrationDocLinks": Object { + "clusterShardLimitExceeded": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#cluster-shard-limit-exceeded", "repeatedTimeoutRequests": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#_repeated_time_out_requests_that_eventually_fail", "resolveMigrationFailures": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html", "routingAllocationDisabled": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#routing-allocation-disabled", diff --git a/src/core/server/saved_objects/migrations/actions/clone_index.ts b/src/core/server/saved_objects/migrations/actions/clone_index.ts index c9496ec6915ca..c64b715468c28 100644 --- a/src/core/server/saved_objects/migrations/actions/clone_index.ts +++ b/src/core/server/saved_objects/migrations/actions/clone_index.ts @@ -23,6 +23,8 @@ import { INDEX_NUMBER_OF_SHARDS, WAIT_FOR_ALL_SHARDS_TO_BE_ACTIVE, } from './constants'; +import { isClusterShardLimitExceeded } from './es_errors'; +import { ClusterShardLimitExceeded } from './create_index'; export type CloneIndexResponse = AcknowledgeResponse; /** @internal */ @@ -49,11 +51,11 @@ export const cloneIndex = ({ target, timeout = DEFAULT_TIMEOUT, }: CloneIndexParams): TaskEither.TaskEither< - RetryableEsClientError | IndexNotFound | IndexNotYellowTimeout, + RetryableEsClientError | IndexNotFound | IndexNotYellowTimeout | ClusterShardLimitExceeded, CloneIndexResponse > => { const cloneTask: TaskEither.TaskEither< - RetryableEsClientError | IndexNotFound, + RetryableEsClientError | IndexNotFound | ClusterShardLimitExceeded, AcknowledgeResponse > = () => { return client.indices @@ -113,6 +115,10 @@ export const cloneIndex = ({ acknowledged: true, shardsAcknowledged: false, }); + } else if (isClusterShardLimitExceeded(error?.body?.error)) { + return Either.left({ + type: 'cluster_shard_limit_exceeded' as const, + }); } else { throw error; } diff --git a/src/core/server/saved_objects/migrations/actions/create_index.ts b/src/core/server/saved_objects/migrations/actions/create_index.ts index 69d47077c0606..3766a470984f5 100644 --- a/src/core/server/saved_objects/migrations/actions/create_index.ts +++ b/src/core/server/saved_objects/migrations/actions/create_index.ts @@ -23,6 +23,7 @@ import { WAIT_FOR_ALL_SHARDS_TO_BE_ACTIVE, } from './constants'; import { IndexNotYellowTimeout, waitForIndexStatusYellow } from './wait_for_index_status_yellow'; +import { isClusterShardLimitExceeded } from './es_errors'; function aliasArrayToRecord(aliases: string[]): Record { const result: Record = {}; @@ -32,6 +33,11 @@ function aliasArrayToRecord(aliases: string[]): Record => { const createIndexTask: TaskEither.TaskEither< - RetryableEsClientError, + RetryableEsClientError | ClusterShardLimitExceeded, AcknowledgeResponse > = () => { const aliasesObject = aliasArrayToRecord(aliases); @@ -120,6 +126,10 @@ export const createIndex = ({ acknowledged: true, shardsAcknowledged: false, }); + } else if (isClusterShardLimitExceeded(error?.body?.error)) { + return Either.left({ + type: 'cluster_shard_limit_exceeded' as const, + }); } else { throw error; } @@ -129,7 +139,11 @@ export const createIndex = ({ return pipe( createIndexTask, - TaskEither.chain((res) => { + TaskEither.chain< + RetryableEsClientError | IndexNotYellowTimeout | ClusterShardLimitExceeded, + AcknowledgeResponse, + 'create_index_succeeded' + >((res) => { if (res.acknowledged && res.shardsAcknowledged) { // If the cluster state was updated and all shards ackd we're done return TaskEither.right('create_index_succeeded'); diff --git a/src/core/server/saved_objects/migrations/actions/es_errors.test.ts b/src/core/server/saved_objects/migrations/actions/es_errors.test.ts index c3a8c7a036a44..b34366b7386d2 100644 --- a/src/core/server/saved_objects/migrations/actions/es_errors.test.ts +++ b/src/core/server/saved_objects/migrations/actions/es_errors.test.ts @@ -6,7 +6,11 @@ * Side Public License, v 1. */ -import { isIncompatibleMappingException, isWriteBlockException } from './es_errors'; +import { + isClusterShardLimitExceeded, + isIncompatibleMappingException, + isWriteBlockException, +} from './es_errors'; describe('isWriteBlockError', () => { it('returns true for a `index write` cluster_block_exception', () => { @@ -54,3 +58,23 @@ describe('isIncompatibleMappingExceptionError', () => { ).toEqual(true); }); }); + +describe('isClusterShardLimitExceeded', () => { + it('returns true with validation_exception and reason is maximum normal shards open', () => { + expect( + isClusterShardLimitExceeded({ + type: 'validation_exception', + reason: + 'Validation Failed: 1: this action would add [2] shards, but this cluster currently has [3]/[1] maximum normal shards open;', + }) + ).toEqual(true); + }); + it('returns false for validation_exception with another reason', () => { + expect( + isClusterShardLimitExceeded({ + type: 'validation_exception', + reason: 'Validation Failed: 1: this action would do something its not allowed to do', + }) + ).toEqual(false); + }); +}); diff --git a/src/core/server/saved_objects/migrations/actions/es_errors.ts b/src/core/server/saved_objects/migrations/actions/es_errors.ts index 4f560468bcb0c..9f571d38ffd85 100644 --- a/src/core/server/saved_objects/migrations/actions/es_errors.ts +++ b/src/core/server/saved_objects/migrations/actions/es_errors.ts @@ -21,3 +21,12 @@ export const isIncompatibleMappingException = ({ type }: estypes.ErrorCause): bo export const isIndexNotFoundException = ({ type }: estypes.ErrorCause): boolean => { return type === 'index_not_found_exception'; }; + +export const isClusterShardLimitExceeded = ({ type, reason }: estypes.ErrorCause): boolean => { + return ( + type === 'validation_exception' && + reason.match( + /this action would add .* shards, but this cluster currently has .* maximum normal shards open/ + ) !== null + ); +}; diff --git a/src/core/server/saved_objects/migrations/actions/index.ts b/src/core/server/saved_objects/migrations/actions/index.ts index 74d8c57ebf171..3a387d764fa4c 100644 --- a/src/core/server/saved_objects/migrations/actions/index.ts +++ b/src/core/server/saved_objects/migrations/actions/index.ts @@ -88,6 +88,7 @@ export { updateAndPickupMappings } from './update_and_pickup_mappings'; import type { UnknownDocsFound } from './check_for_unknown_docs'; import type { IncompatibleClusterRoutingAllocation } from './initialize_action'; +import { ClusterShardLimitExceeded } from './create_index'; export type { CheckForUnknownDocsParams, @@ -153,6 +154,7 @@ export interface ActionErrorTypeMap { unknown_docs_found: UnknownDocsFound; incompatible_cluster_routing_allocation: IncompatibleClusterRoutingAllocation; index_not_yellow_timeout: IndexNotYellowTimeout; + cluster_shard_limit_exceeded: ClusterShardLimitExceeded; } /** diff --git a/src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts index 7ac6911ec7e9e..d47d53aa367e7 100644 --- a/src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrations/actions/integration_tests/actions.test.ts @@ -425,6 +425,10 @@ describe('migration actions', () => { describe('cloneIndex', () => { afterAll(async () => { try { + // Restore the default setting of 1000 shards per node + await client.cluster.putSettings({ + persistent: { cluster: { max_shards_per_node: null } }, + }); await client.indices.delete({ index: 'clone_*' }); } catch (e) { /** ignore */ @@ -577,6 +581,23 @@ describe('migration actions', () => { } `); }); + it('resolves left cluster_shard_limit_exceeded when the action would exceed the maximum normal open shards', async () => { + // Set the max shards per node really low so that any new index that's created would exceed the maximum open shards for this cluster + await client.cluster.putSettings({ persistent: { cluster: { max_shards_per_node: 1 } } }); + const cloneIndexPromise = cloneIndex({ + client, + source: 'existing_index_with_write_block', + target: 'clone_target_4', + })(); + await expect(cloneIndexPromise).resolves.toMatchInlineSnapshot(` + Object { + "_tag": "Left", + "left": Object { + "type": "cluster_shard_limit_exceeded", + }, + } + `); + }); }); // Reindex doesn't return any errors on it's own, so we have to test @@ -1565,6 +1586,10 @@ describe('migration actions', () => { }); describe('createIndex', () => { + afterEach(async () => { + // Restore the default setting of 1000 shards per node + await client.cluster.putSettings({ persistent: { cluster: { max_shards_per_node: null } } }); + }); afterAll(async () => { await client.indices.delete({ index: 'red_then_yellow_index' }); }); @@ -1615,13 +1640,30 @@ describe('migration actions', () => { // Assert that the promise didn't resolve before the index became green expect(indexYellow).toBe(true); expect(res).toMatchInlineSnapshot(` - Object { - "_tag": "Right", - "right": "create_index_succeeded", - } - `); + Object { + "_tag": "Right", + "right": "create_index_succeeded", + } + `); }); }); + it('resolves left cluster_shard_limit_exceeded when the action would exceed the maximum normal open shards', async () => { + // Set the max shards per node really low so that any new index that's created would exceed the maximum open shards for this cluster + await client.cluster.putSettings({ persistent: { cluster: { max_shards_per_node: 1 } } }); + const createIndexPromise = createIndex({ + client, + indexName: 'red_then_yellow_index_1', + mappings: undefined as any, + })(); + await expect(createIndexPromise).resolves.toMatchInlineSnapshot(` + Object { + "_tag": "Left", + "left": Object { + "type": "cluster_shard_limit_exceeded", + }, + } + `); + }); it('rejects when there is an unexpected error creating the index', async () => { // Creating an index with the same name as an existing alias to induce // failure @@ -1646,11 +1688,11 @@ describe('migration actions', () => { }); await expect(task()).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Right", - "right": "bulk_index_succeeded", - } - `); + Object { + "_tag": "Right", + "right": "bulk_index_succeeded", + } + `); }); it('resolves right even if there were some version_conflict_engine_exception', async () => { const existingDocs = ( @@ -1671,11 +1713,11 @@ describe('migration actions', () => { refresh: 'wait_for', }); await expect(task()).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Right", - "right": "bulk_index_succeeded", - } - `); + Object { + "_tag": "Right", + "right": "bulk_index_succeeded", + } + `); }); it('resolves left target_index_had_write_block if there are write_block errors', async () => { const newDocs = [ @@ -1691,13 +1733,13 @@ describe('migration actions', () => { refresh: 'wait_for', })() ).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Left", - "left": Object { - "type": "target_index_had_write_block", - }, - } - `); + Object { + "_tag": "Left", + "left": Object { + "type": "target_index_had_write_block", + }, + } + `); }); it('resolves left request_entity_too_large_exception when the payload is too large', async () => { @@ -1713,13 +1755,13 @@ describe('migration actions', () => { transformedDocs: newDocs, }); await expect(task()).resolves.toMatchInlineSnapshot(` - Object { - "_tag": "Left", - "left": Object { - "type": "request_entity_too_large_exception", - }, - } - `); + Object { + "_tag": "Left", + "left": Object { + "type": "request_entity_too_large_exception", + }, + } + `); }); }); }); diff --git a/src/core/server/saved_objects/migrations/actions/integration_tests/es_errors.test.ts b/src/core/server/saved_objects/migrations/actions/integration_tests/es_errors.test.ts index a409bb31d39ef..8a051835cad67 100644 --- a/src/core/server/saved_objects/migrations/actions/integration_tests/es_errors.test.ts +++ b/src/core/server/saved_objects/migrations/actions/integration_tests/es_errors.test.ts @@ -10,7 +10,7 @@ import { ElasticsearchClient } from '../../../..'; import { InternalCoreStart } from '../../../../internal_types'; import * as kbnTestServer from '../../../../../test_helpers/kbn_server'; import { Root } from '../../../../root'; -import { isWriteBlockException } from '../es_errors'; +import { isWriteBlockException, isClusterShardLimitExceeded } from '../es_errors'; import { createIndex } from '../create_index'; import { setWriteBlock } from '../set_write_block'; @@ -127,4 +127,36 @@ describe('Elasticsearch Errors', () => { expect(isWriteBlockException(cause)).toEqual(true); }); }); + describe('isClusterShardLimitExceeded', () => { + beforeAll(async () => { + await client.cluster.putSettings({ persistent: { cluster: { max_shards_per_node: 1 } } }); + }); + afterAll(async () => { + await client.cluster.putSettings({ persistent: { cluster: { max_shards_per_node: null } } }); + }); + + it('correctly identify errors from create index operation', async () => { + const res = await client.indices.create( + { + index: 'new_test_index', + }, + { ignore: [400] } + ); + + // @ts-expect-error @elastic/elasticsearch doesn't declare error on response + expect(isClusterShardLimitExceeded(res.error)).toEqual(true); + }); + it('correctly identify errors from clone index operation', async () => { + const res = await client.indices.clone( + { + index: 'existing_index_with_write_block', + target: 'new_test_index_2', + }, + { ignore: [400] } + ); + + // @ts-expect-error @elastic/elasticsearch doesn't declare error on response + expect(isClusterShardLimitExceeded(res.error)).toEqual(true); + }); + }); }); diff --git a/src/core/server/saved_objects/migrations/initial_state.test.ts b/src/core/server/saved_objects/migrations/initial_state.test.ts index 2ad3dc38e6d65..c2a2736541208 100644 --- a/src/core/server/saved_objects/migrations/initial_state.test.ts +++ b/src/core/server/saved_objects/migrations/initial_state.test.ts @@ -42,86 +42,161 @@ describe('createInitialState', () => { typeRegistry, docLinks, }) - ).toEqual({ - batchSize: 1000, - maxBatchSizeBytes: ByteSizeValue.parse('100mb').getValueInBytes(), - controlState: 'INIT', - currentAlias: '.kibana_task_manager', - excludeFromUpgradeFilterHooks: {}, - indexPrefix: '.kibana_task_manager', - kibanaVersion: '8.1.0', - knownTypes: [], - legacyIndex: '.kibana_task_manager', - logs: [], - outdatedDocumentsQuery: { - bool: { - should: [], + ).toMatchInlineSnapshot(` + Object { + "batchSize": 1000, + "controlState": "INIT", + "currentAlias": ".kibana_task_manager", + "excludeFromUpgradeFilterHooks": Object {}, + "indexPrefix": ".kibana_task_manager", + "kibanaVersion": "8.1.0", + "knownTypes": Array [], + "legacyIndex": ".kibana_task_manager", + "logs": Array [], + "maxBatchSizeBytes": 104857600, + "migrationDocLinks": Object { + "clusterShardLimitExceeded": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#cluster-shard-limit-exceeded", + "repeatedTimeoutRequests": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#_repeated_time_out_requests_that_eventually_fail", + "resolveMigrationFailures": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html", + "routingAllocationDisabled": "https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#routing-allocation-disabled", }, - }, - preMigrationScript: { - _tag: 'None', - }, - retryAttempts: 15, - retryCount: 0, - retryDelay: 0, - targetIndexMappings: { - dynamic: 'strict', - properties: { - my_type: { - properties: { - title: { - type: 'text', + "outdatedDocumentsQuery": Object { + "bool": Object { + "should": Array [], + }, + }, + "preMigrationScript": Object { + "_tag": "None", + }, + "retryAttempts": 15, + "retryCount": 0, + "retryDelay": 0, + "targetIndexMappings": Object { + "dynamic": "strict", + "properties": Object { + "my_type": Object { + "properties": Object { + "title": Object { + "type": "text", + }, }, }, }, }, - }, - tempIndex: '.kibana_task_manager_8.1.0_reindex_temp', - tempIndexMappings: { - dynamic: false, - properties: { - migrationVersion: { - dynamic: 'true', - type: 'object', - }, - type: { - type: 'keyword', + "tempIndex": ".kibana_task_manager_8.1.0_reindex_temp", + "tempIndexMappings": Object { + "dynamic": false, + "properties": Object { + "migrationVersion": Object { + "dynamic": "true", + "type": "object", + }, + "type": Object { + "type": "keyword", + }, }, }, - }, - unusedTypesQuery: { - bool: { - must_not: expect.arrayContaining([ - { - bool: { - must: [ - { - match: { - type: 'search-session', + "unusedTypesQuery": Object { + "bool": Object { + "must_not": Array [ + Object { + "term": Object { + "type": "apm-services-telemetry", + }, + }, + Object { + "term": Object { + "type": "background-session", + }, + }, + Object { + "term": Object { + "type": "cases-sub-case", + }, + }, + Object { + "term": Object { + "type": "file-upload-telemetry", + }, + }, + Object { + "term": Object { + "type": "fleet-agent-actions", + }, + }, + Object { + "term": Object { + "type": "fleet-agent-events", + }, + }, + Object { + "term": Object { + "type": "fleet-agents", + }, + }, + Object { + "term": Object { + "type": "fleet-enrollment-api-keys", + }, + }, + Object { + "term": Object { + "type": "ml-telemetry", + }, + }, + Object { + "term": Object { + "type": "osquery-usage-metric", + }, + }, + Object { + "term": Object { + "type": "server", + }, + }, + Object { + "term": Object { + "type": "siem-detection-engine-rule-status", + }, + }, + Object { + "term": Object { + "type": "timelion-sheet", + }, + }, + Object { + "term": Object { + "type": "tsvb-validation-telemetry", + }, + }, + Object { + "term": Object { + "type": "ui-counter", + }, + }, + Object { + "bool": Object { + "must": Array [ + Object { + "match": Object { + "type": "search-session", + }, }, - }, - { - match: { - 'search-session.persisted': false, + Object { + "match": Object { + "search-session.persisted": false, + }, }, - }, - ], + ], + }, }, - }, - ]), + ], + }, }, - }, - versionAlias: '.kibana_task_manager_8.1.0', - versionIndex: '.kibana_task_manager_8.1.0_001', - migrationDocLinks: { - resolveMigrationFailures: - 'https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html', - repeatedTimeoutRequests: - 'https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#_repeated_time_out_requests_that_eventually_fail', - routingAllocationDisabled: - 'https://www.elastic.co/guide/en/kibana/test-branch/resolve-migrations-failures.html#routing-allocation-disabled', - }, - }); + "versionAlias": ".kibana_task_manager_8.1.0", + "versionIndex": ".kibana_task_manager_8.1.0_001", + } + `); }); it('returns state with the correct `knownTypes`', () => { diff --git a/src/core/server/saved_objects/migrations/model/model.test.ts b/src/core/server/saved_objects/migrations/model/model.test.ts index e46024fc729d7..1782ece9b4827 100644 --- a/src/core/server/saved_objects/migrations/model/model.test.ts +++ b/src/core/server/saved_objects/migrations/model/model.test.ts @@ -98,6 +98,7 @@ describe('migrations v2 model', () => { resolveMigrationFailures: 'resolveMigrationFailures', repeatedTimeoutRequests: 'repeatedTimeoutRequests', routingAllocationDisabled: 'routingAllocationDisabled', + clusterShardLimitExceeded: 'clusterShardLimitExceeded', }, }; @@ -599,6 +600,16 @@ describe('migrations v2 model', () => { expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); + test('LEGACY_CREATE_REINDEX_TARGET -> FATAL if action fails with cluster_shard_limit_exceeded', () => { + const res: ResponseType<'LEGACY_CREATE_REINDEX_TARGET'> = Either.left({ + type: 'cluster_shard_limit_exceeded', + }); + const newState = model(legacyCreateReindexTargetState, res); + expect(newState.controlState).toEqual('FATAL'); + expect((newState as FatalState).reason).toMatchInlineSnapshot( + `"[cluster_shard_limit_exceeded] Upgrading Kibana requires adding a small number of new shards. Ensure that Kibana is able to add 10 more shards by increasing the cluster.max_shards_per_node setting, or removing indices to clear up resources. See clusterShardLimitExceeded"` + ); + }); }); describe('LEGACY_REINDEX', () => { @@ -997,6 +1008,16 @@ describe('migrations v2 model', () => { expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); + test('CREATE_REINDEX_TEMP -> FATAL if action fails with cluster_shard_limit_exceeded', () => { + const res: ResponseType<'CREATE_REINDEX_TEMP'> = Either.left({ + type: 'cluster_shard_limit_exceeded', + }); + const newState = model(state, res); + expect(newState.controlState).toEqual('FATAL'); + expect((newState as FatalState).reason).toMatchInlineSnapshot( + `"[cluster_shard_limit_exceeded] Upgrading Kibana requires adding a small number of new shards. Ensure that Kibana is able to add 10 more shards by increasing the cluster.max_shards_per_node setting, or removing indices to clear up resources. See clusterShardLimitExceeded"` + ); + }); }); describe('REINDEX_SOURCE_TO_TEMP_OPEN_PIT', () => { @@ -1325,7 +1346,7 @@ describe('migrations v2 model', () => { } `); }); - it('CREATE_NEW_TARGET -> MARK_VERSION_INDEX_READY resets the retry count and delay', () => { + it('CLONE_TEMP_TO_TARGET -> MARK_VERSION_INDEX_READY resets the retry count and delay', () => { const res: ResponseType<'CLONE_TEMP_TO_TARGET'> = Either.right({ acknowledged: true, shardsAcknowledged: true, @@ -1340,6 +1361,16 @@ describe('migrations v2 model', () => { expect(newState.retryCount).toBe(0); expect(newState.retryDelay).toBe(0); }); + test('CLONE_TEMP_TO_TARGET -> FATAL if action fails with cluster_shard_limit_exceeded', () => { + const res: ResponseType<'CLONE_TEMP_TO_TARGET'> = Either.left({ + type: 'cluster_shard_limit_exceeded', + }); + const newState = model(state, res); + expect(newState.controlState).toEqual('FATAL'); + expect((newState as FatalState).reason).toMatchInlineSnapshot( + `"[cluster_shard_limit_exceeded] Upgrading Kibana requires adding a small number of new shards. Ensure that Kibana is able to add 10 more shards by increasing the cluster.max_shards_per_node setting, or removing indices to clear up resources. See clusterShardLimitExceeded"` + ); + }); }); describe('OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT', () => { @@ -1849,6 +1880,16 @@ describe('migrations v2 model', () => { expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); + test('CREATE_NEW_TARGET -> FATAL if action fails with cluster_shard_limit_exceeded', () => { + const res: ResponseType<'CREATE_NEW_TARGET'> = Either.left({ + type: 'cluster_shard_limit_exceeded', + }); + const newState = model(createNewTargetState, res); + expect(newState.controlState).toEqual('FATAL'); + expect((newState as FatalState).reason).toMatchInlineSnapshot( + `"[cluster_shard_limit_exceeded] Upgrading Kibana requires adding a small number of new shards. Ensure that Kibana is able to add 10 more shards by increasing the cluster.max_shards_per_node setting, or removing indices to clear up resources. See clusterShardLimitExceeded"` + ); + }); }); describe('MARK_VERSION_INDEX_READY', () => { diff --git a/src/core/server/saved_objects/migrations/model/model.ts b/src/core/server/saved_objects/migrations/model/model.ts index cbd1941720183..accff9553c808 100644 --- a/src/core/server/saved_objects/migrations/model/model.ts +++ b/src/core/server/saved_objects/migrations/model/model.ts @@ -38,6 +38,7 @@ import { import { createBatches } from './create_batches'; export const FATAL_REASON_REQUEST_ENTITY_TOO_LARGE = `While indexing a batch of saved objects, Elasticsearch returned a 413 Request Entity Too Large exception. Ensure that the Kibana configuration option 'migrations.maxBatchSizeBytes' is set to a value that is lower than or equal to the Elasticsearch 'http.max_content_length' configuration option.`; +const CLUSTER_SHARD_LIMIT_EXCEEDED_REASON = `[cluster_shard_limit_exceeded] Upgrading Kibana requires adding a small number of new shards. Ensure that Kibana is able to add 10 more shards by increasing the cluster.max_shards_per_node setting, or removing indices to clear up resources.`; export const model = (currentState: State, resW: ResponseType): State => { // The action response `resW` is weakly typed, the type includes all action @@ -230,6 +231,12 @@ export const model = (currentState: State, resW: ResponseType): // continue to timeout and eventually lead to a failed migration. const retryErrorMessage = `${left.message} Refer to ${stateP.migrationDocLinks.repeatedTimeoutRequests} for information on how to resolve the issue.`; return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts); + } else if (isLeftTypeof(left, 'cluster_shard_limit_exceeded')) { + return { + ...stateP, + controlState: 'FATAL', + reason: `${CLUSTER_SHARD_LIMIT_EXCEEDED_REASON} See ${stateP.migrationDocLinks.clusterShardLimitExceeded}`, + }; } else { return throwBadResponse(stateP, left); } @@ -447,6 +454,12 @@ export const model = (currentState: State, resW: ResponseType): // continue to timeout and eventually lead to a failed migration. const retryErrorMessage = `${left.message} Refer to ${stateP.migrationDocLinks.repeatedTimeoutRequests} for information on how to resolve the issue.`; return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts); + } else if (isLeftTypeof(left, 'cluster_shard_limit_exceeded')) { + return { + ...stateP, + controlState: 'FATAL', + reason: `${CLUSTER_SHARD_LIMIT_EXCEEDED_REASON} See ${stateP.migrationDocLinks.clusterShardLimitExceeded}`, + }; } else { return throwBadResponse(stateP, left); } @@ -682,6 +695,12 @@ export const model = (currentState: State, resW: ResponseType): // continue to timeout and eventually lead to a failed migration. const retryErrorMessage = `${left.message} Refer to ${stateP.migrationDocLinks.repeatedTimeoutRequests} for information on how to resolve the issue.`; return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts); + } else if (isLeftTypeof(left, 'cluster_shard_limit_exceeded')) { + return { + ...stateP, + controlState: 'FATAL', + reason: `${CLUSTER_SHARD_LIMIT_EXCEEDED_REASON} See ${stateP.migrationDocLinks.clusterShardLimitExceeded}`, + }; } else { throwBadResponse(stateP, left); } @@ -937,6 +956,12 @@ export const model = (currentState: State, resW: ResponseType): // continue to timeout and eventually lead to a failed migration. const retryErrorMessage = `${left.message} Refer to ${stateP.migrationDocLinks.repeatedTimeoutRequests} for information on how to resolve the issue.`; return delayRetryState(stateP, retryErrorMessage, stateP.retryAttempts); + } else if (isLeftTypeof(left, 'cluster_shard_limit_exceeded')) { + return { + ...stateP, + controlState: 'FATAL', + reason: `${CLUSTER_SHARD_LIMIT_EXCEEDED_REASON} See ${stateP.migrationDocLinks.clusterShardLimitExceeded}`, + }; } else { return throwBadResponse(stateP, left); } diff --git a/test/functional/page_objects/login_page.ts b/test/functional/page_objects/login_page.ts index 74e85e60d1a69..51e6c4f7063e6 100644 --- a/test/functional/page_objects/login_page.ts +++ b/test/functional/page_objects/login_page.ts @@ -45,6 +45,9 @@ export class LoginPageObject extends FtrService { } private async regularLogin(user: string, pwd: string) { + if (await this.testSubjects.exists('loginCard-basic/cloud-basic')) { + await this.testSubjects.click('loginCard-basic/cloud-basic'); + } await this.testSubjects.setValue('loginUsername', user); await this.testSubjects.setValue('loginPassword', pwd); await this.testSubjects.click('loginSubmit'); diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index 1416221fc2fe5..b212d4bee63ab 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -203,6 +203,12 @@ exports[`Error SPAN_DURATION 1`] = `undefined`; exports[`Error SPAN_ID 1`] = `undefined`; +exports[`Error SPAN_LINKS 1`] = `undefined`; + +exports[`Error SPAN_LINKS_SPAN_ID 1`] = `undefined`; + +exports[`Error SPAN_LINKS_TRACE_ID 1`] = `undefined`; + exports[`Error SPAN_NAME 1`] = `undefined`; exports[`Error SPAN_SELF_TIME_SUM 1`] = `undefined`; @@ -446,6 +452,12 @@ exports[`Span SPAN_DURATION 1`] = `1337`; exports[`Span SPAN_ID 1`] = `"span id"`; +exports[`Span SPAN_LINKS 1`] = `undefined`; + +exports[`Span SPAN_LINKS_SPAN_ID 1`] = `undefined`; + +exports[`Span SPAN_LINKS_TRACE_ID 1`] = `undefined`; + exports[`Span SPAN_NAME 1`] = `"span name"`; exports[`Span SPAN_SELF_TIME_SUM 1`] = `undefined`; @@ -703,6 +715,12 @@ exports[`Transaction SPAN_DURATION 1`] = `undefined`; exports[`Transaction SPAN_ID 1`] = `undefined`; +exports[`Transaction SPAN_LINKS 1`] = `undefined`; + +exports[`Transaction SPAN_LINKS_SPAN_ID 1`] = `undefined`; + +exports[`Transaction SPAN_LINKS_TRACE_ID 1`] = `undefined`; + exports[`Transaction SPAN_NAME 1`] = `undefined`; exports[`Transaction SPAN_SELF_TIME_SUM 1`] = `undefined`; diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index aaf864b3bf75b..e4970ea2ac067 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -74,6 +74,10 @@ export const SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT = export const SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM = 'span.destination.service.response_time.sum.us'; +export const SPAN_LINKS = 'span.links'; +export const SPAN_LINKS_TRACE_ID = 'span.links.trace.id'; +export const SPAN_LINKS_SPAN_ID = 'span.links.span.id'; + // Parent ID for a transaction or span export const PARENT_ID = 'parent.id'; diff --git a/x-pack/plugins/apm/common/span_links.ts b/x-pack/plugins/apm/common/span_links.ts new file mode 100644 index 0000000000000..cd5ce48e6802a --- /dev/null +++ b/x-pack/plugins/apm/common/span_links.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AgentName } from '../typings/es_schemas/ui/fields/agent'; +import { Environment } from './environment_rt'; + +export interface SpanLinkDetails { + traceId: string; + spanId: string; + details?: { + agentName: AgentName; + serviceName: string; + duration: number; + environment: Environment; + transactionId?: string; + spanName?: string; + spanSubtype?: string; + spanType?: string; + }; +} diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/fixtures/es_archiver/apm_8.0.0_empty/mappings.json b/x-pack/plugins/apm/ftr_e2e/cypress/fixtures/es_archiver/apm_8.0.0_empty/mappings.json index 2d05717fa5725..8e9d447af8966 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/fixtures/es_archiver/apm_8.0.0_empty/mappings.json +++ b/x-pack/plugins/apm/ftr_e2e/cypress/fixtures/es_archiver/apm_8.0.0_empty/mappings.json @@ -1,1139 +1,3 @@ -{ - "type": "index", - "value": { - "aliases": { - ".ml-anomalies-.write-apm-environment_not_defined-337d-high_mean_transaction_duration": { - "is_hidden": true - }, - ".ml-anomalies-.write-apm-production-6117-high_mean_transaction_duration": { - "is_hidden": true - }, - ".ml-anomalies-.write-apm-testing-41e5-high_mean_transaction_duration": { - "is_hidden": true - }, - ".ml-anomalies-apm-environment_not_defined-337d-high_mean_transaction_duration": { - "filter": { - "term": { - "job_id": { - "boost": 1, - "value": "apm-environment_not_defined-337d-high_mean_transaction_duration" - } - } - }, - "is_hidden": true - }, - ".ml-anomalies-apm-production-6117-high_mean_transaction_duration": { - "filter": { - "term": { - "job_id": { - "boost": 1, - "value": "apm-production-6117-high_mean_transaction_duration" - } - } - }, - "is_hidden": true - }, - ".ml-anomalies-apm-testing-41e5-high_mean_transaction_duration": { - "filter": { - "term": { - "job_id": { - "boost": 1, - "value": "apm-testing-41e5-high_mean_transaction_duration" - } - } - }, - "is_hidden": true - } - }, - "index": ".ml-anomalies-shared", - "mappings": { - "_meta": { - "version": "7.14.0" - }, - "dynamic_templates": [ - { - "strings_as_keywords": { - "mapping": { - "type": "keyword" - }, - "match": "*" - } - } - ], - "properties": { - "actual": { - "type": "double" - }, - "all_field_values": { - "analyzer": "whitespace", - "type": "text" - }, - "anomaly_score": { - "type": "double" - }, - "assignment_memory_basis": { - "type": "keyword" - }, - "average_bucket_processing_time_ms": { - "type": "double" - }, - "bucket_allocation_failures_count": { - "type": "long" - }, - "bucket_count": { - "type": "long" - }, - "bucket_influencers": { - "properties": { - "anomaly_score": { - "type": "double" - }, - "bucket_span": { - "type": "long" - }, - "influencer_field_name": { - "type": "keyword" - }, - "initial_anomaly_score": { - "type": "double" - }, - "is_interim": { - "type": "boolean" - }, - "job_id": { - "type": "keyword" - }, - "probability": { - "type": "double" - }, - "raw_anomaly_score": { - "type": "double" - }, - "result_type": { - "type": "keyword" - }, - "timestamp": { - "type": "date" - } - }, - "type": "nested" - }, - "bucket_span": { - "type": "long" - }, - "by_field_name": { - "type": "keyword" - }, - "by_field_value": { - "copy_to": [ - "all_field_values" - ], - "type": "keyword" - }, - "categorization_status": { - "type": "keyword" - }, - "categorized_doc_count": { - "type": "keyword" - }, - "category_id": { - "type": "long" - }, - "causes": { - "properties": { - "actual": { - "type": "double" - }, - "by_field_name": { - "type": "keyword" - }, - "by_field_value": { - "copy_to": [ - "all_field_values" - ], - "type": "keyword" - }, - "correlated_by_field_value": { - "copy_to": [ - "all_field_values" - ], - "type": "keyword" - }, - "field_name": { - "type": "keyword" - }, - "function": { - "type": "keyword" - }, - "function_description": { - "type": "keyword" - }, - "geo_results": { - "properties": { - "actual_point": { - "type": "geo_point" - }, - "typical_point": { - "type": "geo_point" - } - } - }, - "over_field_name": { - "type": "keyword" - }, - "over_field_value": { - "copy_to": [ - "all_field_values" - ], - "type": "keyword" - }, - "partition_field_name": { - "type": "keyword" - }, - "partition_field_value": { - "copy_to": [ - "all_field_values" - ], - "type": "keyword" - }, - "probability": { - "type": "double" - }, - "typical": { - "type": "double" - } - }, - "type": "nested" - }, - "dead_category_count": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "detector_index": { - "type": "integer" - }, - "earliest_record_timestamp": { - "type": "date" - }, - "empty_bucket_count": { - "type": "long" - }, - "event_count": { - "type": "long" - }, - "examples": { - "type": "text" - }, - "exponential_average_bucket_processing_time_ms": { - "type": "double" - }, - "exponential_average_calculation_context": { - "properties": { - "incremental_metric_value_ms": { - "type": "double" - }, - "latest_timestamp": { - "type": "date" - }, - "previous_exponential_average_ms": { - "type": "double" - } - } - }, - "failed_category_count": { - "type": "keyword" - }, - "field_name": { - "type": "keyword" - }, - "forecast_create_timestamp": { - "type": "date" - }, - "forecast_end_timestamp": { - "type": "date" - }, - "forecast_expiry_timestamp": { - "type": "date" - }, - "forecast_id": { - "type": "keyword" - }, - "forecast_lower": { - "type": "double" - }, - "forecast_memory_bytes": { - "type": "long" - }, - "forecast_messages": { - "type": "keyword" - }, - "forecast_prediction": { - "type": "double" - }, - "forecast_progress": { - "type": "double" - }, - "forecast_start_timestamp": { - "type": "date" - }, - "forecast_status": { - "type": "keyword" - }, - "forecast_upper": { - "type": "double" - }, - "frequent_category_count": { - "type": "keyword" - }, - "function": { - "type": "keyword" - }, - "function_description": { - "type": "keyword" - }, - "geo_results": { - "properties": { - "actual_point": { - "type": "geo_point" - }, - "typical_point": { - "type": "geo_point" - } - } - }, - "influencer_field_name": { - "type": "keyword" - }, - "influencer_field_value": { - "copy_to": [ - "all_field_values" - ], - "type": "keyword" - }, - "influencer_score": { - "type": "double" - }, - "influencers": { - "properties": { - "influencer_field_name": { - "type": "keyword" - }, - "influencer_field_values": { - "copy_to": [ - "all_field_values" - ], - "type": "keyword" - } - }, - "type": "nested" - }, - "initial_anomaly_score": { - "type": "double" - }, - "initial_influencer_score": { - "type": "double" - }, - "initial_record_score": { - "type": "double" - }, - "input_bytes": { - "type": "long" - }, - "input_field_count": { - "type": "long" - }, - "input_record_count": { - "type": "long" - }, - "invalid_date_count": { - "type": "long" - }, - "is_interim": { - "type": "boolean" - }, - "job_id": { - "copy_to": [ - "all_field_values" - ], - "type": "keyword" - }, - "last_data_time": { - "type": "date" - }, - "latest_empty_bucket_timestamp": { - "type": "date" - }, - "latest_record_time_stamp": { - "type": "date" - }, - "latest_record_timestamp": { - "type": "date" - }, - "latest_result_time_stamp": { - "type": "date" - }, - "latest_sparse_bucket_timestamp": { - "type": "date" - }, - "log_time": { - "type": "date" - }, - "max_matching_length": { - "type": "long" - }, - "maximum_bucket_processing_time_ms": { - "type": "double" - }, - "memory_status": { - "type": "keyword" - }, - "min_version": { - "type": "keyword" - }, - "minimum_bucket_processing_time_ms": { - "type": "double" - }, - "missing_field_count": { - "type": "long" - }, - "mlcategory": { - "type": "keyword" - }, - "model_bytes": { - "type": "long" - }, - "model_bytes_exceeded": { - "type": "keyword" - }, - "model_bytes_memory_limit": { - "type": "keyword" - }, - "model_feature": { - "type": "keyword" - }, - "model_lower": { - "type": "double" - }, - "model_median": { - "type": "double" - }, - "model_size_stats": { - "properties": { - "assignment_memory_basis": { - "type": "keyword" - }, - "bucket_allocation_failures_count": { - "type": "long" - }, - "categorization_status": { - "type": "keyword" - }, - "categorized_doc_count": { - "type": "keyword" - }, - "dead_category_count": { - "type": "keyword" - }, - "failed_category_count": { - "type": "keyword" - }, - "frequent_category_count": { - "type": "keyword" - }, - "job_id": { - "type": "keyword" - }, - "log_time": { - "type": "date" - }, - "memory_status": { - "type": "keyword" - }, - "model_bytes": { - "type": "long" - }, - "model_bytes_exceeded": { - "type": "keyword" - }, - "model_bytes_memory_limit": { - "type": "keyword" - }, - "peak_model_bytes": { - "type": "long" - }, - "rare_category_count": { - "type": "keyword" - }, - "result_type": { - "type": "keyword" - }, - "timestamp": { - "type": "date" - }, - "total_by_field_count": { - "type": "long" - }, - "total_category_count": { - "type": "keyword" - }, - "total_over_field_count": { - "type": "long" - }, - "total_partition_field_count": { - "type": "long" - } - } - }, - "model_upper": { - "type": "double" - }, - "multi_bucket_impact": { - "type": "double" - }, - "num_matches": { - "type": "long" - }, - "out_of_order_timestamp_count": { - "type": "long" - }, - "over_field_name": { - "type": "keyword" - }, - "over_field_value": { - "copy_to": [ - "all_field_values" - ], - "type": "keyword" - }, - "partition_field_name": { - "type": "keyword" - }, - "partition_field_value": { - "copy_to": [ - "all_field_values" - ], - "type": "keyword" - }, - "peak_model_bytes": { - "type": "keyword" - }, - "preferred_to_categories": { - "type": "long" - }, - "probability": { - "type": "double" - }, - "processed_field_count": { - "type": "long" - }, - "processed_record_count": { - "type": "long" - }, - "processing_time_ms": { - "type": "long" - }, - "quantiles": { - "enabled": false, - "type": "object" - }, - "rare_category_count": { - "type": "keyword" - }, - "raw_anomaly_score": { - "type": "double" - }, - "record_score": { - "type": "double" - }, - "regex": { - "type": "keyword" - }, - "result_type": { - "type": "keyword" - }, - "retain": { - "type": "boolean" - }, - "scheduled_events": { - "type": "keyword" - }, - "search_count": { - "type": "long" - }, - "service": { - "properties": { - "name": { - "type": "keyword" - } - } - }, - "snapshot_doc_count": { - "type": "integer" - }, - "snapshot_id": { - "type": "keyword" - }, - "sparse_bucket_count": { - "type": "long" - }, - "terms": { - "type": "text" - }, - "timestamp": { - "type": "date" - }, - "total_by_field_count": { - "type": "long" - }, - "total_category_count": { - "type": "keyword" - }, - "total_over_field_count": { - "type": "long" - }, - "total_partition_field_count": { - "type": "long" - }, - "total_search_time_ms": { - "type": "double" - }, - "transaction": { - "properties": { - "type": { - "type": "keyword" - } - } - }, - "typical": { - "type": "double" - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "hidden": "true", - "number_of_replicas": "1", - "number_of_shards": "1", - "translog": { - "durability": "async" - } - } - } - } -} - -{ - "type": "index", - "value": { - "aliases": { - }, - "index": ".ml-config", - "mappings": { - "_meta": { - "version": "7.14.0" - }, - "dynamic_templates": [ - { - "strings_as_keywords": { - "mapping": { - "type": "keyword" - }, - "match": "*" - } - } - ], - "properties": { - "aggregations": { - "enabled": false, - "type": "object" - }, - "allow_lazy_open": { - "type": "keyword" - }, - "allow_lazy_start": { - "type": "keyword" - }, - "analysis": { - "properties": { - "classification": { - "properties": { - "alpha": { - "type": "double" - }, - "class_assignment_objective": { - "type": "keyword" - }, - "dependent_variable": { - "type": "keyword" - }, - "downsample_factor": { - "type": "double" - }, - "early_stopping_enabled": { - "type": "boolean" - }, - "eta": { - "type": "double" - }, - "eta_growth_rate_per_tree": { - "type": "double" - }, - "feature_bag_fraction": { - "type": "double" - }, - "feature_processors": { - "enabled": false, - "type": "object" - }, - "gamma": { - "type": "double" - }, - "lambda": { - "type": "double" - }, - "max_optimization_rounds_per_hyperparameter": { - "type": "integer" - }, - "max_trees": { - "type": "integer" - }, - "num_top_classes": { - "type": "integer" - }, - "num_top_feature_importance_values": { - "type": "integer" - }, - "prediction_field_name": { - "type": "keyword" - }, - "randomize_seed": { - "type": "keyword" - }, - "soft_tree_depth_limit": { - "type": "double" - }, - "soft_tree_depth_tolerance": { - "type": "double" - }, - "training_percent": { - "type": "double" - } - } - }, - "outlier_detection": { - "properties": { - "compute_feature_influence": { - "type": "keyword" - }, - "feature_influence_threshold": { - "type": "double" - }, - "method": { - "type": "keyword" - }, - "n_neighbors": { - "type": "integer" - }, - "outlier_fraction": { - "type": "keyword" - }, - "standardization_enabled": { - "type": "keyword" - } - } - }, - "regression": { - "properties": { - "alpha": { - "type": "double" - }, - "dependent_variable": { - "type": "keyword" - }, - "downsample_factor": { - "type": "double" - }, - "early_stopping_enabled": { - "type": "boolean" - }, - "eta": { - "type": "double" - }, - "eta_growth_rate_per_tree": { - "type": "double" - }, - "feature_bag_fraction": { - "type": "double" - }, - "feature_processors": { - "enabled": false, - "type": "object" - }, - "gamma": { - "type": "double" - }, - "lambda": { - "type": "double" - }, - "loss_function": { - "type": "keyword" - }, - "loss_function_parameter": { - "type": "double" - }, - "max_optimization_rounds_per_hyperparameter": { - "type": "integer" - }, - "max_trees": { - "type": "integer" - }, - "num_top_feature_importance_values": { - "type": "integer" - }, - "prediction_field_name": { - "type": "keyword" - }, - "randomize_seed": { - "type": "keyword" - }, - "soft_tree_depth_limit": { - "type": "double" - }, - "soft_tree_depth_tolerance": { - "type": "double" - }, - "training_percent": { - "type": "double" - } - } - } - } - }, - "analysis_config": { - "properties": { - "bucket_span": { - "type": "keyword" - }, - "categorization_analyzer": { - "enabled": false, - "type": "object" - }, - "categorization_field_name": { - "type": "keyword" - }, - "categorization_filters": { - "type": "keyword" - }, - "detectors": { - "properties": { - "by_field_name": { - "type": "keyword" - }, - "custom_rules": { - "properties": { - "actions": { - "type": "keyword" - }, - "conditions": { - "properties": { - "applies_to": { - "type": "keyword" - }, - "operator": { - "type": "keyword" - }, - "value": { - "type": "double" - } - }, - "type": "nested" - }, - "scope": { - "enabled": false, - "type": "object" - } - }, - "type": "nested" - }, - "detector_description": { - "type": "text" - }, - "detector_index": { - "type": "integer" - }, - "exclude_frequent": { - "type": "keyword" - }, - "field_name": { - "type": "keyword" - }, - "function": { - "type": "keyword" - }, - "over_field_name": { - "type": "keyword" - }, - "partition_field_name": { - "type": "keyword" - }, - "use_null": { - "type": "boolean" - } - } - }, - "influencers": { - "type": "keyword" - }, - "latency": { - "type": "keyword" - }, - "multivariate_by_fields": { - "type": "boolean" - }, - "per_partition_categorization": { - "properties": { - "enabled": { - "type": "boolean" - }, - "stop_on_warn": { - "type": "boolean" - } - } - }, - "summary_count_field_name": { - "type": "keyword" - } - } - }, - "analysis_limits": { - "properties": { - "categorization_examples_limit": { - "type": "long" - }, - "model_memory_limit": { - "type": "keyword" - } - } - }, - "analyzed_fields": { - "enabled": false, - "type": "object" - }, - "background_persist_interval": { - "type": "keyword" - }, - "blocked": { - "properties": { - "reason": { - "type": "keyword" - }, - "task_id": { - "type": "keyword" - } - } - }, - "chunking_config": { - "properties": { - "mode": { - "type": "keyword" - }, - "time_span": { - "type": "keyword" - } - } - }, - "config_type": { - "type": "keyword" - }, - "create_time": { - "type": "date" - }, - "custom_settings": { - "enabled": false, - "type": "object" - }, - "daily_model_snapshot_retention_after_days": { - "type": "long" - }, - "data_description": { - "properties": { - "field_delimiter": { - "type": "keyword" - }, - "format": { - "type": "keyword" - }, - "quote_character": { - "type": "keyword" - }, - "time_field": { - "type": "keyword" - }, - "time_format": { - "type": "keyword" - } - } - }, - "datafeed_id": { - "type": "keyword" - }, - "delayed_data_check_config": { - "properties": { - "check_window": { - "type": "keyword" - }, - "enabled": { - "type": "boolean" - } - } - }, - "deleting": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "dest": { - "properties": { - "index": { - "type": "keyword" - }, - "results_field": { - "type": "keyword" - } - } - }, - "finished_time": { - "type": "date" - }, - "frequency": { - "type": "keyword" - }, - "groups": { - "type": "keyword" - }, - "headers": { - "enabled": false, - "type": "object" - }, - "id": { - "type": "keyword" - }, - "indices": { - "type": "keyword" - }, - "indices_options": { - "enabled": false, - "type": "object" - }, - "job_id": { - "type": "keyword" - }, - "job_type": { - "type": "keyword" - }, - "job_version": { - "type": "keyword" - }, - "max_empty_searches": { - "type": "keyword" - }, - "max_num_threads": { - "type": "integer" - }, - "model_memory_limit": { - "type": "keyword" - }, - "model_plot_config": { - "properties": { - "annotations_enabled": { - "type": "boolean" - }, - "enabled": { - "type": "boolean" - }, - "terms": { - "type": "keyword" - } - } - }, - "model_snapshot_id": { - "type": "keyword" - }, - "model_snapshot_min_version": { - "type": "keyword" - }, - "model_snapshot_retention_days": { - "type": "long" - }, - "query": { - "enabled": false, - "type": "object" - }, - "query_delay": { - "type": "keyword" - }, - "renormalization_window_days": { - "type": "long" - }, - "results_index_name": { - "type": "keyword" - }, - "results_retention_days": { - "type": "long" - }, - "runtime_mappings": { - "enabled": false, - "type": "object" - }, - "script_fields": { - "enabled": false, - "type": "object" - }, - "scroll_size": { - "type": "long" - }, - "source": { - "properties": { - "_source": { - "enabled": false, - "type": "object" - }, - "index": { - "type": "keyword" - }, - "query": { - "enabled": false, - "type": "object" - }, - "runtime_mappings": { - "enabled": false, - "type": "object" - } - } - }, - "version": { - "type": "keyword" - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "blocks": { - "read_only_allow_delete": "false" - }, - "max_result_window": "10000", - "number_of_replicas": "1", - "number_of_shards": "1" - } - } - } -} - { "type": "index", "value": { @@ -16155,6 +15019,30 @@ } } }, + "links": { + "dynamic": "false", + "type": "nested", + "properties": { + "trace": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "span": { + "dynamic": "false", + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, "transaction": { "dynamic": "false", "properties": { diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/fixtures/es_archiver/apm_mappings_only_8.0.0/mappings.json b/x-pack/plugins/apm/ftr_e2e/cypress/fixtures/es_archiver/apm_mappings_only_8.0.0/mappings.json index 2d05717fa5725..3167ad3f5a6a0 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/fixtures/es_archiver/apm_mappings_only_8.0.0/mappings.json +++ b/x-pack/plugins/apm/ftr_e2e/cypress/fixtures/es_archiver/apm_mappings_only_8.0.0/mappings.json @@ -628,8 +628,7 @@ { "type": "index", "value": { - "aliases": { - }, + "aliases": {}, "index": ".ml-config", "mappings": { "_meta": { @@ -15510,6 +15509,26 @@ "type": { "ignore_above": 1024, "type": "keyword" + }, + "links": { + "properties": { + "span": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "trace": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } } } }, @@ -20620,6 +20639,26 @@ "type": { "ignore_above": 1024, "type": "keyword" + }, + "links": { + "properties": { + "span": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "trace": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } } } }, diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/generate_span_links_data.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/generate_span_links_data.ts new file mode 100644 index 0000000000000..3905cf324c44a --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/generate_span_links_data.ts @@ -0,0 +1,326 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { apm, EntityArrayIterable, timerange } from '@elastic/apm-synthtrace'; +import { synthtrace } from '../../../../synthtrace'; +import { SpanLink } from '../../../../../typings/es_schemas/raw/fields/span_links'; + +function getProducerInternalOnly() { + const producerInternalOnlyInstance = apm + .service('producer-internal-only', 'production', 'go') + .instance('instance a'); + + const events = timerange( + new Date('2022-01-01T00:00:00.000Z'), + new Date('2022-01-01T00:01:00.000Z') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return producerInternalOnlyInstance + .transaction(`Transaction A`) + .timestamp(timestamp) + .duration(1000) + .success() + .children( + producerInternalOnlyInstance + .span(`Span A`, 'external', 'http') + .timestamp(timestamp + 50) + .duration(100) + .success() + ); + }); + + const apmFields = events.toArray(); + const transactionA = apmFields.find( + (item) => item['processor.event'] === 'transaction' + ); + const spanA = apmFields.find((item) => item['processor.event'] === 'span'); + + const ids = + spanA && transactionA + ? { + transactionAId: transactionA['transaction.id']!, + traceId: spanA['trace.id']!, + spanAId: spanA['span.id']!, + } + : {}; + const spanASpanLink = spanA + ? { trace: { id: spanA['trace.id']! }, span: { id: spanA['span.id']! } } + : undefined; + + return { + ids, + spanASpanLink, + apmFields, + }; +} + +function getProducerExternalOnly() { + const producerExternalOnlyInstance = apm + .service('producer-external-only', 'production', 'java') + .instance('instance b'); + + const events = timerange( + new Date('2022-01-01T00:02:00.000Z'), + new Date('2022-01-01T00:03:00.000Z') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return producerExternalOnlyInstance + .transaction(`Transaction B`) + .timestamp(timestamp) + .duration(1000) + .success() + .children( + producerExternalOnlyInstance + .span(`Span B`, 'external', 'http') + .defaults({ + 'span.links': [ + { trace: { id: 'trace#1' }, span: { id: 'span#1' } }, + ], + }) + .timestamp(timestamp + 50) + .duration(100) + .success(), + producerExternalOnlyInstance + .span(`Span B.1`, 'external', 'http') + .timestamp(timestamp + 50) + .duration(100) + .success() + ); + }); + + const apmFields = events.toArray(); + const transactionB = apmFields.find( + (item) => item['processor.event'] === 'transaction' + ); + const spanB = apmFields.find( + (item) => + item['processor.event'] === 'span' && item['span.name'] === 'Span B' + ); + const ids = + spanB && transactionB + ? { + traceId: spanB['trace.id']!, + transactionBId: transactionB['transaction.id']!, + spanBId: spanB['span.id']!, + } + : {}; + + const spanBSpanLink = spanB + ? { + trace: { id: spanB['trace.id']! }, + span: { id: spanB['span.id']! }, + } + : undefined; + + return { + ids, + spanBSpanLink, + apmFields, + }; +} + +function getProducerConsumer({ + producerInternalOnlySpanASpanLink, +}: { + producerInternalOnlySpanASpanLink?: SpanLink; +}) { + const producerConsumerInstance = apm + .service('producer-consumer', 'production', 'ruby') + .instance('instance c'); + + const events = timerange( + new Date('2022-01-01T00:04:00.000Z'), + new Date('2022-01-01T00:05:00.000Z') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return producerConsumerInstance + .transaction(`Transaction C`) + .defaults({ + 'span.links': producerInternalOnlySpanASpanLink + ? [producerInternalOnlySpanASpanLink] + : [], + }) + .timestamp(timestamp) + .duration(1000) + .success() + .children( + producerConsumerInstance + .span(`Span C`, 'external', 'http') + .timestamp(timestamp + 50) + .duration(100) + .success() + ); + }); + + const apmFields = events.toArray(); + const transactionC = apmFields.find( + (item) => item['processor.event'] === 'transaction' + ); + const transactionCSpanLink = transactionC + ? { + trace: { id: transactionC['trace.id']! }, + span: { id: transactionC['transaction.id']! }, + } + : undefined; + const spanC = apmFields.find( + (item) => + item['processor.event'] === 'span' || item['span.name'] === 'Span C' + ); + const spanCSpanLink = spanC + ? { + trace: { id: spanC['trace.id']! }, + span: { id: spanC['span.id']! }, + } + : undefined; + const ids = + spanC && transactionC + ? { + traceId: transactionC['trace.id']!, + transactionCId: transactionC['transaction.id']!, + spanCId: spanC['span.id']!, + } + : {}; + return { + transactionCSpanLink, + spanCSpanLink, + ids, + apmFields, + }; +} + +function getConsumerMultiple({ + producerInternalOnlySpanASpanLink, + producerExternalOnlySpanBSpanLink, + producerConsumerSpanCSpanLink, + producerConsumerTransactionCSpanLink, +}: { + producerInternalOnlySpanASpanLink?: SpanLink; + producerExternalOnlySpanBSpanLink?: SpanLink; + producerConsumerSpanCSpanLink?: SpanLink; + producerConsumerTransactionCSpanLink?: SpanLink; +}) { + const consumerMultipleInstance = apm + .service('consumer-multiple', 'production', 'nodejs') + .instance('instance d'); + + const events = timerange( + new Date('2022-01-01T00:06:00.000Z'), + new Date('2022-01-01T00:07:00.000Z') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return consumerMultipleInstance + .transaction(`Transaction D`) + .defaults({ + 'span.links': + producerInternalOnlySpanASpanLink && producerConsumerSpanCSpanLink + ? [ + producerInternalOnlySpanASpanLink, + producerConsumerSpanCSpanLink, + ] + : [], + }) + .timestamp(timestamp) + .duration(1000) + .success() + .children( + consumerMultipleInstance + .span(`Span E`, 'external', 'http') + .defaults({ + 'span.links': + producerExternalOnlySpanBSpanLink && + producerConsumerTransactionCSpanLink + ? [ + producerExternalOnlySpanBSpanLink, + producerConsumerTransactionCSpanLink, + ] + : [], + }) + .timestamp(timestamp + 50) + .duration(100) + .success() + ); + }); + const apmFields = events.toArray(); + const transactionD = apmFields.find( + (item) => item['processor.event'] === 'transaction' + ); + const spanE = apmFields.find((item) => item['processor.event'] === 'span'); + + const ids = + transactionD && spanE + ? { + traceId: transactionD['trace.id']!, + transactionDId: transactionD['transaction.id']!, + spanEId: spanE['span.id']!, + } + : {}; + + return { + ids, + apmFields, + }; +} + +/** + * Data ingestion summary: + * + * producer-internal-only (go) + * --Transaction A + * ----Span A + * + * producer-external-only (java) + * --Transaction B + * ----Span B + * ------span.links=external link + * ----Span B1 + * + * producer-consumer (ruby) + * --Transaction C + * ------span.links=producer-internal-only / Span A + * ----Span C + * + * consumer-multiple (nodejs) + * --Transaction D + * ------span.links= producer-consumer / Span C | producer-internal-only / Span A + * ----Span E + * ------span.links= producer-external-only / Span B | producer-consumer / Transaction C + */ +export async function generateSpanLinksData() { + const producerInternalOnly = getProducerInternalOnly(); + const producerExternalOnly = getProducerExternalOnly(); + const producerConsumer = getProducerConsumer({ + producerInternalOnlySpanASpanLink: producerInternalOnly.spanASpanLink, + }); + const producerMultiple = getConsumerMultiple({ + producerInternalOnlySpanASpanLink: producerInternalOnly.spanASpanLink, + producerConsumerSpanCSpanLink: producerConsumer.spanCSpanLink, + producerConsumerTransactionCSpanLink: producerConsumer.transactionCSpanLink, + producerExternalOnlySpanBSpanLink: producerExternalOnly.spanBSpanLink, + }); + + await synthtrace.index( + new EntityArrayIterable(producerInternalOnly.apmFields).merge( + new EntityArrayIterable(producerExternalOnly.apmFields), + new EntityArrayIterable(producerConsumer.apmFields), + new EntityArrayIterable(producerMultiple.apmFields) + ) + ); + + return { + producerInternalOnlyIds: producerInternalOnly.ids, + producerExternalOnlyIds: producerExternalOnly.ids, + producerConsumerIds: producerConsumer.ids, + producerMultipleIds: producerMultiple.ids, + }; +} diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/span_links.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/span_links.spec.ts new file mode 100644 index 0000000000000..99efb7b6a2b6b --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transaction_details/span_links.spec.ts @@ -0,0 +1,301 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import url from 'url'; +import { synthtrace } from '../../../../synthtrace'; +import { generateSpanLinksData } from './generate_span_links_data'; + +const start = '2022-01-01T00:00:00.000Z'; +const end = '2022-01-01T00:15:00.000Z'; + +function getServiceInventoryUrl({ serviceName }: { serviceName: string }) { + return url.format({ + pathname: `/app/apm/services/${serviceName}`, + query: { + rangeFrom: start, + rangeTo: end, + environment: 'ENVIRONMENT_ALL', + kuery: '', + serviceGroup: '', + transactionType: 'request', + comparisonEnabled: true, + offset: '1d', + }, + }); +} + +describe('Span links', () => { + beforeEach(() => { + cy.loginAsReadOnlyUser(); + }); + + describe('when data is loaded', () => { + let ids: Awaited>; + before(async () => { + ids = await generateSpanLinksData(); + }); + + after(async () => { + await synthtrace.clean(); + }); + + describe('span links count on trace waterfall', () => { + it('Shows two children and no parents on producer-internal-only Span A', () => { + cy.visit( + getServiceInventoryUrl({ serviceName: 'producer-internal-only' }) + ); + cy.contains('Transaction A').click(); + cy.contains('2 Span links'); + cy.get( + `[data-test-subj="spanLinksBadge_${ids.producerInternalOnlyIds.spanAId}"]` + ).realHover(); + cy.contains('2 Span links found'); + cy.contains('2 incoming'); + cy.contains('0 outgoing'); + }); + + it('Shows one parent and one children on producer-external-only Span B', () => { + cy.visit( + getServiceInventoryUrl({ serviceName: 'producer-external-only' }) + ); + cy.contains('Transaction B').click(); + cy.contains('2 Span links'); + cy.get( + `[data-test-subj="spanLinksBadge_${ids.producerExternalOnlyIds.spanBId}"]` + ).realHover(); + cy.contains('2 Span links found'); + cy.contains('1 incoming'); + cy.contains('1 outgoing'); + }); + + it('Shows one parent and one children on producer-consumer Transaction C', () => { + cy.visit(getServiceInventoryUrl({ serviceName: 'producer-consumer' })); + cy.contains('Transaction C').click(); + cy.contains('2 Span links'); + cy.get( + `[data-test-subj="spanLinksBadge_${ids.producerConsumerIds.transactionCId}"]` + ).realHover(); + cy.contains('2 Span links found'); + cy.contains('1 incoming'); + cy.contains('1 outgoing'); + }); + + it('Shows no parent and one children on producer-consumer Span C', () => { + cy.visit(getServiceInventoryUrl({ serviceName: 'producer-consumer' })); + cy.contains('Transaction C').click(); + cy.contains('1 Span link'); + cy.get( + `[data-test-subj="spanLinksBadge_${ids.producerConsumerIds.spanCId}"]` + ).realHover(); + cy.contains('1 Span link found'); + cy.contains('1 incoming'); + cy.contains('0 outgoing'); + }); + + it('Shows two parents and one children on consumer-multiple Transaction D', () => { + cy.visit(getServiceInventoryUrl({ serviceName: 'consumer-multiple' })); + cy.contains('Transaction D').click(); + cy.contains('2 Span links'); + cy.get( + `[data-test-subj="spanLinksBadge_${ids.producerMultipleIds.transactionDId}"]` + ).realHover(); + cy.contains('2 Span links found'); + cy.contains('0 incoming'); + cy.contains('2 outgoing'); + }); + + it('Shows two parents and one children on consumer-multiple Span E', () => { + cy.visit(getServiceInventoryUrl({ serviceName: 'consumer-multiple' })); + cy.contains('Transaction D').click(); + cy.contains('2 Span links'); + cy.get( + `[data-test-subj="spanLinksBadge_${ids.producerMultipleIds.spanEId}"]` + ).realHover(); + cy.contains('2 Span links found'); + cy.contains('0 incoming'); + cy.contains('2 outgoing'); + }); + }); + + describe('span link flyout', () => { + it('Shows children details on producer-internal-only Span A', () => { + cy.visit( + getServiceInventoryUrl({ serviceName: 'producer-internal-only' }) + ); + cy.contains('Transaction A').click(); + cy.contains('Span A').click(); + cy.get('[data-test-subj="spanLinksTab"]').click(); + cy.contains('producer-consumer') + .should('have.attr', 'href') + .and('include', '/services/producer-consumer/overview'); + cy.contains('Transaction C') + .should('have.attr', 'href') + .and( + 'include', + `/link-to/transaction/${ids.producerConsumerIds.transactionCId}?waterfallItemId=${ids.producerConsumerIds.transactionCId}` + ); + cy.contains('consumer-multiple') + .should('have.attr', 'href') + .and('include', '/services/consumer-multiple/overview'); + cy.contains('Transaction D') + .should('have.attr', 'href') + .and( + 'include', + `link-to/transaction/${ids.producerMultipleIds.transactionDId}?waterfallItemId=${ids.producerMultipleIds.transactionDId}` + ); + cy.get('[data-test-subj="spanLinkTypeSelect"]').should( + 'contain.text', + 'Outgoing links (0)' + ); + }); + + it('Shows children and parents details on producer-external-only Span B', () => { + cy.visit( + getServiceInventoryUrl({ serviceName: 'producer-external-only' }) + ); + cy.contains('Transaction B').click(); + cy.contains('Span B').click(); + cy.get('[data-test-subj="spanLinksTab"]').click(); + + cy.contains('consumer-multiple') + .should('have.attr', 'href') + .and('include', '/services/consumer-multiple/overview'); + cy.contains('Span E') + .should('have.attr', 'href') + .and( + 'include', + `link-to/transaction/${ids.producerMultipleIds.transactionDId}?waterfallItemId=${ids.producerMultipleIds.spanEId}` + ); + cy.get('[data-test-subj="spanLinkTypeSelect"]').select( + 'Outgoing links (1)' + ); + cy.contains('Unknown'); + cy.contains('trace#1-span#1'); + }); + + it('Shows children and parents details on producer-consumer Transaction C', () => { + cy.visit(getServiceInventoryUrl({ serviceName: 'producer-consumer' })); + cy.contains('Transaction C').click(); + cy.get( + `[aria-controls="${ids.producerConsumerIds.transactionCId}"]` + ).click(); + cy.get('[data-test-subj="spanLinksTab"]').click(); + + cy.contains('consumer-multiple') + .should('have.attr', 'href') + .and('include', '/services/consumer-multiple/overview'); + cy.contains('Span E') + .should('have.attr', 'href') + .and( + 'include', + `link-to/transaction/${ids.producerMultipleIds.transactionDId}?waterfallItemId=${ids.producerMultipleIds.spanEId}` + ); + + cy.get('[data-test-subj="spanLinkTypeSelect"]').select( + 'Outgoing links (1)' + ); + cy.contains('producer-internal-only') + .should('have.attr', 'href') + .and('include', '/services/producer-internal-only/overview'); + cy.contains('Span A') + .should('have.attr', 'href') + .and( + 'include', + `link-to/transaction/${ids.producerInternalOnlyIds.transactionAId}?waterfallItemId=${ids.producerInternalOnlyIds.spanAId}` + ); + }); + + it('Shows children and parents details on producer-consumer Span C', () => { + cy.visit(getServiceInventoryUrl({ serviceName: 'producer-consumer' })); + cy.contains('Transaction C').click(); + cy.contains('Span C').click(); + cy.get('[data-test-subj="spanLinksTab"]').click(); + + cy.contains('consumer-multiple') + .should('have.attr', 'href') + .and('include', '/services/consumer-multiple/overview'); + cy.contains('Transaction D') + .should('have.attr', 'href') + .and( + 'include', + `link-to/transaction/${ids.producerMultipleIds.transactionDId}?waterfallItemId=${ids.producerMultipleIds.transactionDId}` + ); + + cy.get('[data-test-subj="spanLinkTypeSelect"]').should( + 'contain.text', + 'Outgoing links (0)' + ); + }); + + it('Shows children and parents details on consumer-multiple Transaction D', () => { + cy.visit(getServiceInventoryUrl({ serviceName: 'consumer-multiple' })); + cy.contains('Transaction D').click(); + cy.get( + `[aria-controls="${ids.producerMultipleIds.transactionDId}"]` + ).click(); + cy.get('[data-test-subj="spanLinksTab"]').click(); + + cy.contains('producer-consumer') + .should('have.attr', 'href') + .and('include', '/services/producer-consumer/overview'); + cy.contains('Span C') + .should('have.attr', 'href') + .and( + 'include', + `link-to/transaction/${ids.producerConsumerIds.transactionCId}?waterfallItemId=${ids.producerConsumerIds.spanCId}` + ); + + cy.contains('producer-internal-only') + .should('have.attr', 'href') + .and('include', '/services/producer-internal-only/overview'); + cy.contains('Span A') + .should('have.attr', 'href') + .and( + 'include', + `link-to/transaction/${ids.producerInternalOnlyIds.transactionAId}?waterfallItemId=${ids.producerInternalOnlyIds.spanAId}` + ); + + cy.get('[data-test-subj="spanLinkTypeSelect"]').should( + 'contain.text', + 'Incoming links (0)' + ); + }); + + it('Shows children and parents details on consumer-multiple Span E', () => { + cy.visit(getServiceInventoryUrl({ serviceName: 'consumer-multiple' })); + cy.contains('Transaction D').click(); + cy.contains('Span E').click(); + cy.get('[data-test-subj="spanLinksTab"]').click(); + + cy.contains('producer-external-only') + .should('have.attr', 'href') + .and('include', '/services/producer-external-only/overview'); + cy.contains('Span B') + .should('have.attr', 'href') + .and( + 'include', + `link-to/transaction/${ids.producerExternalOnlyIds.transactionBId}?waterfallItemId=${ids.producerExternalOnlyIds.spanBId}` + ); + + cy.contains('producer-consumer') + .should('have.attr', 'href') + .and('include', '/services/producer-consumer/overview'); + cy.contains('Transaction C') + .should('have.attr', 'href') + .and( + 'include', + `link-to/transaction/${ids.producerConsumerIds.transactionCId}?waterfallItemId=${ids.producerConsumerIds.transactionCId}` + ); + + cy.get('[data-test-subj="spanLinkTypeSelect"]').should( + 'contain.text', + 'Incoming links (0)' + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/trace_link/get_redirect_to_transaction_detail_page_url.test.ts b/x-pack/plugins/apm/public/components/app/trace_link/get_redirect_to_transaction_detail_page_url.test.ts index 56a4facd20496..2b95ffc28680d 100644 --- a/x-pack/plugins/apm/public/components/app/trace_link/get_redirect_to_transaction_detail_page_url.test.ts +++ b/x-pack/plugins/apm/public/components/app/trace_link/get_redirect_to_transaction_detail_page_url.test.ts @@ -34,7 +34,7 @@ describe('getRedirectToTransactionDetailPageUrl', () => { it('formats url correctly', () => { expect(url).toBe( - '/services/opbeans-node/transactions/view?traceId=trace_id&transactionId=transaction_id&transactionName=transaction_name&transactionType=request&rangeFrom=2020-01-01T00%3A00%3A00.000Z&rangeTo=2020-01-01T00%3A05%3A00.000Z' + '/services/opbeans-node/transactions/view?traceId=trace_id&transactionId=transaction_id&transactionName=transaction_name&transactionType=request&rangeFrom=2020-01-01T00%3A00%3A00.000Z&rangeTo=2020-01-01T00%3A05%3A00.000Z&waterfallItemId=' ); }); }); @@ -48,7 +48,7 @@ describe('getRedirectToTransactionDetailPageUrl', () => { it('uses timerange provided', () => { expect(url).toBe( - '/services/opbeans-node/transactions/view?traceId=trace_id&transactionId=transaction_id&transactionName=transaction_name&transactionType=request&rangeFrom=2020-01-01T00%3A02%3A00.000Z&rangeTo=2020-01-01T00%3A17%3A59.999Z' + '/services/opbeans-node/transactions/view?traceId=trace_id&transactionId=transaction_id&transactionName=transaction_name&transactionType=request&rangeFrom=2020-01-01T00%3A02%3A00.000Z&rangeTo=2020-01-01T00%3A17%3A59.999Z&waterfallItemId=' ); }); }); diff --git a/x-pack/plugins/apm/public/components/app/trace_link/get_redirect_to_transaction_detail_page_url.ts b/x-pack/plugins/apm/public/components/app/trace_link/get_redirect_to_transaction_detail_page_url.ts index c78c08d6f37dd..a3467d7272ff5 100644 --- a/x-pack/plugins/apm/public/components/app/trace_link/get_redirect_to_transaction_detail_page_url.ts +++ b/x-pack/plugins/apm/public/components/app/trace_link/get_redirect_to_transaction_detail_page_url.ts @@ -12,10 +12,12 @@ export const getRedirectToTransactionDetailPageUrl = ({ transaction, rangeFrom, rangeTo, + waterfallItemId, }: { transaction: Transaction; rangeFrom?: string; rangeTo?: string; + waterfallItemId?: string; }) => { return format({ pathname: `/services/${transaction.service.name}/transactions/view`, @@ -37,6 +39,7 @@ export const getRedirectToTransactionDetailPageUrl = ({ diff: transaction.transaction.duration.us / 1000, direction: 'up', }), + waterfallItemId, }, }); }; diff --git a/x-pack/plugins/apm/public/components/app/trace_link/trace_link.test.tsx b/x-pack/plugins/apm/public/components/app/trace_link/trace_link.test.tsx index d8a5127af21a3..432262bb79b11 100644 --- a/x-pack/plugins/apm/public/components/app/trace_link/trace_link.test.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_link/trace_link.test.tsx @@ -122,7 +122,7 @@ describe('TraceLink', () => { const component = shallow(); expect(component.prop('to')).toEqual( - '/services/foo/transactions/view?traceId=123&transactionId=456&transactionName=bar&transactionType=GET&rangeFrom=now-24h&rangeTo=now' + '/services/foo/transactions/view?traceId=123&transactionId=456&transactionName=bar&transactionType=GET&rangeFrom=now-24h&rangeTo=now&waterfallItemId=' ); }); }); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts b/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts index dea15b952a41b..72d52ae09c9bd 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts @@ -10,12 +10,14 @@ import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_ import { useApmParams } from '../../../hooks/use_apm_params'; import { useFetcher } from '../../../hooks/use_fetcher'; import { useTimeRange } from '../../../hooks/use_time_range'; +import { APIReturnType } from '../../../services/rest/create_call_apm_api'; import { getWaterfall } from './waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers'; -const INITIAL_DATA = { +const INITIAL_DATA: APIReturnType<'GET /internal/apm/traces/{traceId}'> = { errorDocs: [], traceDocs: [], exceedsMax: false, + linkedChildrenOfSpanCountBySpanId: {}, }; export function useWaterfallFetcher() { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/span_links_badge.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/span_links_badge.tsx new file mode 100644 index 0000000000000..194c3ec38bc9e --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/span_links_badge.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { SpanLinksCount } from '../waterfall_helpers/waterfall_helpers'; + +type Props = SpanLinksCount & { id: string }; + +export function SpanLinksBadge({ linkedParents, linkedChildren, id }: Props) { + if (!linkedParents && !linkedChildren) { + return null; + } + + const total = linkedParents + linkedChildren; + return ( + + + {i18n.translate( + 'xpack.apm.waterfall.spanLinks.tooltip.linkedChildren', + { + defaultMessage: '{linkedChildren} incoming', + values: { linkedChildren }, + } + )} + + + {i18n.translate( + 'xpack.apm.waterfall.spanLinks.tooltip.linkedParents', + { + defaultMessage: '{linkedParents} outgoing', + values: { linkedParents }, + } + )} + + + } + > + + {i18n.translate('xpack.apm.waterfall.spanLinks.badge', { + defaultMessage: + '{total} {total, plural, one {Span link} other {Span links}}', + values: { total }, + })} + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx index d50305c22e543..825da91c7d385 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx @@ -20,24 +20,27 @@ import { EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { Fragment } from 'react'; -import { isEmpty } from 'lodash'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; -import { CompositeSpanDurationSummaryItem } from '../../../../../../shared/summary/composite_span_duration_summary_item'; +import { isEmpty } from 'lodash'; +import React, { Fragment } from 'react'; import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; import { DiscoverSpanLink } from '../../../../../../shared/links/discover_links/discover_span_link'; import { SpanMetadata } from '../../../../../../shared/metadata_table/span_metadata'; +import { getSpanLinksTabContent } from '../../../../../../shared/span_links/span_links_tab_content'; import { Stacktrace } from '../../../../../../shared/stacktrace'; import { Summary } from '../../../../../../shared/summary'; +import { CompositeSpanDurationSummaryItem } from '../../../../../../shared/summary/composite_span_duration_summary_item'; import { DurationSummaryItem } from '../../../../../../shared/summary/duration_summary_item'; import { HttpInfoSummaryItem } from '../../../../../../shared/summary/http_info_summary_item'; import { TimestampTooltip } from '../../../../../../shared/timestamp_tooltip'; -import { ResponsiveFlyout } from '../responsive_flyout'; import { SyncBadge } from '../badge/sync_badge'; +import { FailureBadge } from '../failure_badge'; +import { ResponsiveFlyout } from '../responsive_flyout'; +import { SpanLinksCount } from '../waterfall_helpers/waterfall_helpers'; import { SpanDatabase } from './span_db'; import { StickySpanProperties } from './sticky_span_properties'; -import { FailureBadge } from '../failure_badge'; +import { ProcessorEvent } from '../../../../../../../../common/processor_event'; function formatType(type: string) { switch (type) { @@ -86,6 +89,7 @@ interface Props { parentTransaction?: Transaction; totalDuration?: number; onClose: () => void; + spanLinksCount: SpanLinksCount; } export function SpanFlyout({ @@ -93,6 +97,7 @@ export function SpanFlyout({ parentTransaction, totalDuration, onClose, + spanLinksCount, }: Props) { if (!span) { return null; @@ -107,6 +112,13 @@ export function SpanFlyout({ const spanHttpUrl = span.url?.original || span.span?.http?.url?.original; const spanHttpMethod = span.http?.request?.method || span.span?.http?.method; + const spanLinksTabContent = getSpanLinksTabContent({ + spanLinksCount, + traceId: span.trace.id, + spanId: span.span.id, + processorEvent: ProcessorEvent.span, + }); + return ( @@ -254,6 +266,7 @@ export function SpanFlyout({ }, ] : []), + ...(spanLinksTabContent ? [spanLinksTabContent] : []), ]} /> diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/transaction_flyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/transaction_flyout/index.tsx index fd68fde81fb60..9e8661fd523fb 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/transaction_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/transaction_flyout/index.tsx @@ -10,19 +10,23 @@ import { EuiFlexItem, EuiFlyoutBody, EuiFlyoutHeader, + EuiHorizontalRule, EuiPortal, EuiSpacer, + EuiTabbedContent, EuiTitle, - EuiHorizontalRule, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { ProcessorEvent } from '../../../../../../../../common/processor_event'; import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; -import { TransactionActionMenu } from '../../../../../../shared/transaction_action_menu/transaction_action_menu'; +import { TransactionMetadata } from '../../../../../../shared/metadata_table/transaction_metadata'; +import { getSpanLinksTabContent } from '../../../../../../shared/span_links/span_links_tab_content'; import { TransactionSummary } from '../../../../../../shared/summary/transaction_summary'; +import { TransactionActionMenu } from '../../../../../../shared/transaction_action_menu/transaction_action_menu'; import { FlyoutTopLevelProperties } from '../flyout_top_level_properties'; import { ResponsiveFlyout } from '../responsive_flyout'; -import { TransactionMetadata } from '../../../../../../shared/metadata_table/transaction_metadata'; +import { SpanLinksCount } from '../waterfall_helpers/waterfall_helpers'; import { DroppedSpansWarning } from './dropped_spans_warning'; interface Props { @@ -30,21 +34,7 @@ interface Props { transaction?: Transaction; errorCount?: number; rootTransactionDuration?: number; -} - -function TransactionPropertiesTable({ - transaction, -}: { - transaction: Transaction; -}) { - return ( -
- -

Metadata

-
- -
- ); + spanLinksCount: SpanLinksCount; } export function TransactionFlyout({ @@ -52,11 +42,19 @@ export function TransactionFlyout({ onClose, errorCount = 0, rootTransactionDuration, + spanLinksCount, }: Props) { if (!transactionDoc) { return null; } + const spanLinksTabContent = getSpanLinksTabContent({ + spanLinksCount, + traceId: transactionDoc.trace.id, + spanId: transactionDoc.transaction.id, + processorEvent: ProcessorEvent.transaction, + }); + return ( @@ -94,7 +92,26 @@ export function TransactionFlyout({ /> - + + + + + ), + }, + ...(spanLinksTabContent ? [spanLinksTabContent] : []), + ]} + /> diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_flyout.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_flyout.tsx index 948f790848e8f..bd0ab44e0e208 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_flyout.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_flyout.tsx @@ -45,6 +45,7 @@ export function WaterfallFlyout({ span={currentItem.doc} parentTransaction={parentTransaction} onClose={() => toggleFlyout({ history })} + spanLinksCount={currentItem.spanLinksCount} /> ); case 'transaction': @@ -56,6 +57,7 @@ export function WaterfallFlyout({ waterfall.rootTransaction?.transaction.duration.us } errorCount={waterfall.getErrorCount(currentItem.id)} + spanLinksCount={currentItem.spanLinksCount} /> ); default: diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap index b1ea74c3eb0c0..ad296743c6031 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap @@ -190,18 +190,38 @@ Object { "parent": undefined, "parentId": undefined, "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId1", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "mySpanIdD", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId2", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "mySpanIdA", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, Object { "color": "", @@ -389,18 +409,38 @@ Object { "parent": undefined, "parentId": undefined, "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId1", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "mySpanIdD", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId2", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "mySpanIdA", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, ], "mySpanIdD": Array [ @@ -516,12 +556,24 @@ Object { "parent": undefined, "parentId": undefined, "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId1", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "mySpanIdD", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, ], "myTransactionId1": Array [ @@ -596,9 +648,17 @@ Object { "parent": undefined, "parentId": undefined, "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId1", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, ], "myTransactionId2": Array [ @@ -751,15 +811,31 @@ Object { "parent": undefined, "parentId": undefined, "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId1", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "mySpanIdD", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId2", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, ], "root": Array [ @@ -797,6 +873,10 @@ Object { "parent": undefined, "parentId": undefined, "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, ], }, @@ -835,6 +915,10 @@ Object { "parent": undefined, "parentId": undefined, "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "errorItems": Array [ Object { @@ -907,6 +991,10 @@ Object { "parent": undefined, "parentId": undefined, "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId1", "skew": 0, @@ -948,6 +1036,10 @@ Object { "parent": undefined, "parentId": undefined, "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, Object { "color": "", @@ -1020,9 +1112,17 @@ Object { "parent": undefined, "parentId": undefined, "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId1", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, Object { "color": "", @@ -1136,12 +1236,24 @@ Object { "parent": undefined, "parentId": undefined, "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId1", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "mySpanIdD", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, Object { "color": "", @@ -1292,15 +1404,31 @@ Object { "parent": undefined, "parentId": undefined, "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId1", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "mySpanIdD", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId2", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, Object { "color": "", @@ -1488,18 +1616,38 @@ Object { "parent": undefined, "parentId": undefined, "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId1", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "mySpanIdD", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId2", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "mySpanIdA", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, Object { "color": "", @@ -1687,18 +1835,38 @@ Object { "parent": undefined, "parentId": undefined, "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId1", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "mySpanIdD", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId2", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "mySpanIdA", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, ], "legends": Array [ @@ -1869,12 +2037,24 @@ Object { "parent": undefined, "parentId": "mySpanIdD", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId2", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "mySpanIdA", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, Object { "color": "", @@ -1994,12 +2174,24 @@ Object { "parent": undefined, "parentId": "mySpanIdD", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId2", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "mySpanIdA", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, ], "mySpanIdD": Array [ @@ -2047,6 +2239,10 @@ Object { "parent": undefined, "parentId": "mySpanIdD", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, ], "myTransactionId2": Array [ @@ -2131,9 +2327,17 @@ Object { "parent": undefined, "parentId": "mySpanIdD", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId2", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, ], }, @@ -2182,6 +2386,10 @@ Object { "parent": undefined, "parentId": "mySpanIdD", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "errorItems": Array [], "getErrorCount": [Function], @@ -2230,6 +2438,10 @@ Object { "parent": undefined, "parentId": "mySpanIdD", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, Object { "color": "", @@ -2312,9 +2524,17 @@ Object { "parent": undefined, "parentId": "mySpanIdD", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId2", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, Object { "color": "", @@ -2434,12 +2654,24 @@ Object { "parent": undefined, "parentId": "mySpanIdD", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId2", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "mySpanIdA", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, Object { "color": "", @@ -2559,12 +2791,24 @@ Object { "parent": undefined, "parentId": "mySpanIdD", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "myTransactionId2", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "mySpanIdA", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, ], "legends": Array [ @@ -2687,6 +2931,10 @@ Array [ "offset": 0, "parent": undefined, "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, Object { "color": "", @@ -2740,9 +2988,17 @@ Array [ "offset": 0, "parent": undefined, "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "a", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, Object { "color": "", @@ -2796,9 +3052,17 @@ Array [ "offset": 0, "parent": undefined, "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "a", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, Object { "color": "", @@ -2877,12 +3141,24 @@ Array [ "offset": 0, "parent": undefined, "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "a", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "b", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, Object { "color": "", @@ -2989,15 +3265,31 @@ Array [ "offset": 0, "parent": undefined, "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "a", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "b", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, "parentId": "c", "skew": 0, + "spanLinksCount": Object { + "linkedChildren": 0, + "linkedParents": 0, + }, }, ] `; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers.test.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers.test.ts index 8c96c48f47d7c..1aa38688c1549 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers.test.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers.test.ts @@ -133,6 +133,7 @@ describe('waterfall_helpers', () => { traceDocs: hits, errorDocs, exceedsMax: false, + linkedChildrenOfSpanCountBySpanId: {}, }; const waterfall = getWaterfall(apiResp, entryTransactionId); const { apiResponse, ...waterfallRest } = waterfall; @@ -151,6 +152,7 @@ describe('waterfall_helpers', () => { traceDocs: hits, errorDocs, exceedsMax: false, + linkedChildrenOfSpanCountBySpanId: {}, }; const waterfall = getWaterfall(apiResp, entryTransactionId); @@ -236,6 +238,7 @@ describe('waterfall_helpers', () => { traceDocs: traceItems, errorDocs: [], exceedsMax: false, + linkedChildrenOfSpanCountBySpanId: {}, }, entryTransactionId ); @@ -342,6 +345,7 @@ describe('waterfall_helpers', () => { traceDocs: traceItems, errorDocs: [], exceedsMax: false, + linkedChildrenOfSpanCountBySpanId: {}, }, entryTransactionId ); @@ -404,6 +408,10 @@ describe('waterfall_helpers', () => { skew: 0, legendValues, color: '', + spanLinksCount: { + linkedChildren: 0, + linkedParents: 0, + }, }, { docType: 'span', @@ -426,6 +434,10 @@ describe('waterfall_helpers', () => { skew: 0, legendValues, color: '', + spanLinksCount: { + linkedChildren: 0, + linkedParents: 0, + }, }, { docType: 'span', @@ -448,6 +460,10 @@ describe('waterfall_helpers', () => { skew: 0, legendValues, color: '', + spanLinksCount: { + linkedChildren: 0, + linkedParents: 0, + }, }, { docType: 'transaction', @@ -464,6 +480,10 @@ describe('waterfall_helpers', () => { skew: 0, legendValues, color: '', + spanLinksCount: { + linkedChildren: 0, + linkedParents: 0, + }, }, { docType: 'transaction', @@ -481,6 +501,10 @@ describe('waterfall_helpers', () => { skew: 0, legendValues, color: '', + spanLinksCount: { + linkedChildren: 0, + linkedParents: 0, + }, }, ]; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers.ts index 489eb30528bf4..cf53606afd9b4 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers.ts @@ -7,10 +7,11 @@ import { euiPaletteColorBlind } from '@elastic/eui'; import { first, flatten, groupBy, isEmpty, sortBy, uniq } from 'lodash'; -import { APIReturnType } from '../../../../../../../services/rest/create_call_apm_api'; -import { APMError } from '../../../../../../../../typings/es_schemas/ui/apm_error'; -import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; -import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; +import type { APIReturnType } from '../../../../../../../services/rest/create_call_apm_api'; +import type { APMError } from '../../../../../../../../typings/es_schemas/ui/apm_error'; +import type { Span } from '../../../../../../../../typings/es_schemas/ui/span'; +import type { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; +import { ProcessorEvent } from '../../../../../../../../common/processor_event'; type TraceAPIResponse = APIReturnType<'GET /internal/apm/traces/{traceId}'>; @@ -20,6 +21,11 @@ interface IWaterfallGroup { const ROOT_ID = 'root'; +export interface SpanLinksCount { + linkedChildren: number; + linkedParents: number; +} + export enum WaterfallLegendType { ServiceName = 'serviceName', SpanType = 'spanType', @@ -48,6 +54,7 @@ interface IWaterfallSpanItemBase */ duration: number; legendValues: Record; + spanLinksCount: SpanLinksCount; } interface IWaterfallItemBase { @@ -93,13 +100,17 @@ function getLegendValues(transactionOrSpan: Transaction | Span) { return { [WaterfallLegendType.ServiceName]: transactionOrSpan.service.name, [WaterfallLegendType.SpanType]: - 'span' in transactionOrSpan - ? transactionOrSpan.span.subtype || transactionOrSpan.span.type + transactionOrSpan.processor.event === ProcessorEvent.span + ? (transactionOrSpan as Span).span.subtype || + (transactionOrSpan as Span).span.type : '', }; } -function getTransactionItem(transaction: Transaction): IWaterfallTransaction { +function getTransactionItem( + transaction: Transaction, + linkedChildrenCount: number = 0 +): IWaterfallTransaction { return { docType: 'transaction', doc: transaction, @@ -110,10 +121,17 @@ function getTransactionItem(transaction: Transaction): IWaterfallTransaction { skew: 0, legendValues: getLegendValues(transaction), color: '', + spanLinksCount: { + linkedParents: transaction.span?.links?.length ?? 0, + linkedChildren: linkedChildrenCount, + }, }; } -function getSpanItem(span: Span): IWaterfallSpan { +function getSpanItem( + span: Span, + linkedChildrenCount: number = 0 +): IWaterfallSpan { return { docType: 'span', doc: span, @@ -124,6 +142,10 @@ function getSpanItem(span: Span): IWaterfallSpan { skew: 0, legendValues: getLegendValues(span), color: '', + spanLinksCount: { + linkedParents: span.span.links?.length ?? 0, + linkedChildren: linkedChildrenCount, + }, }; } @@ -265,14 +287,26 @@ const getWaterfallDuration = (waterfallItems: IWaterfallItem[]) => 0 ); -const getWaterfallItems = (items: TraceAPIResponse['traceDocs']) => +const getWaterfallItems = ( + items: TraceAPIResponse['traceDocs'], + linkedChildrenOfSpanCountBySpanId: TraceAPIResponse['linkedChildrenOfSpanCountBySpanId'] +) => items.map((item) => { const docType: 'span' | 'transaction' = item.processor.event; switch (docType) { - case 'span': - return getSpanItem(item as Span); + case 'span': { + const span = item as Span; + return getSpanItem( + span, + linkedChildrenOfSpanCountBySpanId[span.span.id] + ); + } case 'transaction': - return getTransactionItem(item as Transaction); + const transaction = item as Transaction; + return getTransactionItem( + transaction, + linkedChildrenOfSpanCountBySpanId[transaction.transaction.id] + ); } }); @@ -396,7 +430,8 @@ export function getWaterfall( const errorCountByParentId = getErrorCountByParentId(apiResponse.errorDocs); const waterfallItems: IWaterfallSpanOrTransaction[] = getWaterfallItems( - apiResponse.traceDocs + apiResponse.traceDocs, + apiResponse.linkedChildrenOfSpanCountBySpanId ); const childrenByParentId = getChildrenGroupedByParentId( diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx index 9190fd5924174..d372cec9ce16d 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx @@ -19,6 +19,7 @@ import { asDuration } from '../../../../../../../common/utils/formatters'; import { Margins } from '../../../../../shared/charts/timeline'; import { TruncateWithTooltip } from '../../../../../shared/truncate_with_tooltip'; import { SyncBadge } from './badge/sync_badge'; +import { SpanLinksBadge } from './badge/span_links_badge'; import { ColdStartBadge } from './badge/cold_start_badge'; import { IWaterfallSpanOrTransaction } from './waterfall_helpers/waterfall_helpers'; import { FailureBadge } from './failure_badge'; @@ -237,6 +238,11 @@ export function WaterfallItem({ agentName={item.doc.agent.name} /> )} + {isServerlessColdstart && } diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.data.ts b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.data.ts index d4af0e92c9054..9ac9497a33d6a 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.data.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.data.ts @@ -530,6 +530,7 @@ export const simpleTrace = { ], exceedsMax: false, errorDocs: [], + linkedChildrenOfSpanCountBySpanId: {}, } as TraceAPIResponse; export const manyChildrenWithSameLength = { @@ -4126,6 +4127,7 @@ export const manyChildrenWithSameLength = { }, }, ], + linkedChildrenOfSpanCountBySpanId: {}, } as TraceAPIResponse; export const traceWithErrors = { @@ -4716,6 +4718,7 @@ export const traceWithErrors = { }, }, ], + linkedChildrenOfSpanCountBySpanId: {}, } as unknown as TraceAPIResponse; export const traceChildStartBeforeParent = { @@ -5211,6 +5214,7 @@ export const traceChildStartBeforeParent = { ], exceedsMax: false, errorDocs: [], + linkedChildrenOfSpanCountBySpanId: {}, } as TraceAPIResponse; export const inferredSpans = { @@ -5865,4 +5869,5 @@ export const inferredSpans = { ], exceedsMax: false, errorDocs: [], + linkedChildrenOfSpanCountBySpanId: {}, } as TraceAPIResponse; diff --git a/x-pack/plugins/apm/public/components/app/transaction_link/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_link/index.tsx index b28649d9bc56f..720d3feee581a 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_link/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_link/index.tsx @@ -21,7 +21,7 @@ const CentralizedContainer = euiStyled.div` export function TransactionLink() { const { path: { transactionId }, - query: { rangeFrom, rangeTo }, + query: { rangeFrom, rangeTo, waterfallItemId }, } = useApmParams('/link-to/transaction/{transactionId}'); const { data = { transaction: null }, status } = useFetcher( @@ -46,6 +46,7 @@ export function TransactionLink() { transaction: data.transaction, rangeFrom, rangeTo, + waterfallItemId, })} /> ); diff --git a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx index ad6947c736fe2..fe5e0742f7c2c 100644 --- a/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx +++ b/x-pack/plugins/apm/public/components/routing/apm_route_config.tsx @@ -43,6 +43,7 @@ const apmRoutes = { query: t.partial({ rangeFrom: t.string, rangeTo: t.string, + waterfallItemId: t.string, }), }), ]), diff --git a/x-pack/plugins/apm/public/components/shared/span_links/index.tsx b/x-pack/plugins/apm/public/components/shared/span_links/index.tsx new file mode 100644 index 0000000000000..0dc0213e92d44 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/span_links/index.tsx @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiSelect, + EuiSelectOption, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useMemo, useState } from 'react'; +import { useApmParams } from '../../../hooks/use_apm_params'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { useTimeRange } from '../../../hooks/use_time_range'; +import { SpanLinksCount } from '../../app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers'; +import { KueryBar } from '../kuery_bar'; +import { SpanLinksCallout } from './span_links_callout'; +import { SpanLinksTable } from './span_links_table'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { useLocalStorage } from '../../../hooks/use_local_storage'; + +interface Props { + spanLinksCount: SpanLinksCount; + traceId: string; + spanId: string; + processorEvent: ProcessorEvent; +} + +type LinkType = 'children' | 'parents'; + +export function SpanLinks({ + spanLinksCount, + traceId, + spanId, + processorEvent, +}: Props) { + const { + query: { rangeFrom, rangeTo }, + } = useApmParams('/services/{serviceName}/transactions/view'); + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); + + const [selectedLinkType, setSelectedLinkType] = useState( + spanLinksCount.linkedChildren ? 'children' : 'parents' + ); + + const [spanLinksCalloutDismissed, setSpanLinksCalloutDismissed] = + useLocalStorage('apm.spanLinksCalloutDismissed', false); + + const [kuery, setKuery] = useState(''); + + const { data, status } = useFetcher( + (callApmApi) => { + if (selectedLinkType === 'children') { + return callApmApi( + 'GET /internal/apm/traces/{traceId}/span_links/{spanId}/children', + { + params: { + path: { traceId, spanId }, + query: { kuery, start, end }, + }, + } + ); + } + return callApmApi( + 'GET /internal/apm/traces/{traceId}/span_links/{spanId}/parents', + { + params: { + path: { traceId, spanId }, + query: { kuery, start, end, processorEvent }, + }, + } + ); + }, + [selectedLinkType, kuery, traceId, spanId, start, end, processorEvent] + ); + + const selectOptions: EuiSelectOption[] = useMemo( + () => [ + { + value: 'children', + text: i18n.translate('xpack.apm.spanLinks.combo.childrenLinks', { + defaultMessage: 'Incoming links ({linkedChildren})', + values: { linkedChildren: spanLinksCount.linkedChildren }, + }), + disabled: !spanLinksCount.linkedChildren, + }, + { + value: 'parents', + text: i18n.translate('xpack.apm.spanLinks.combo.parentsLinks', { + defaultMessage: 'Outgoing links ({linkedParents})', + values: { linkedParents: spanLinksCount.linkedParents }, + }), + disabled: !spanLinksCount.linkedParents, + }, + ], + [spanLinksCount] + ); + + if ( + !data || + status === FETCH_STATUS.LOADING || + status === FETCH_STATUS.NOT_INITIATED + ) { + return ( +
+ +
+ ); + } + + return ( + + {!spanLinksCalloutDismissed && ( + + { + setSpanLinksCalloutDismissed(true); + }} + /> + + )} + + + + { + setKuery(value); + }} + value={kuery} + /> + + + { + setSelectedLinkType(e.target.value as LinkType); + }} + /> + + + + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/span_links/span_links_callout.tsx b/x-pack/plugins/apm/public/components/shared/span_links/span_links_callout.tsx new file mode 100644 index 0000000000000..1884db40a2111 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/span_links/span_links_callout.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiButton, EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; + +interface Props { + dismissCallout: () => void; +} + +export function SpanLinksCallout({ dismissCallout }: Props) { + return ( + +

+ +

+ { + dismissCallout(); + }} + > + {i18n.translate('xpack.apm.spanLinks.callout.dimissButton', { + defaultMessage: 'Dismiss', + })} + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/shared/span_links/span_links_tab_content.tsx b/x-pack/plugins/apm/public/components/shared/span_links/span_links_tab_content.tsx new file mode 100644 index 0000000000000..ce20491d4b472 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/span_links/span_links_tab_content.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiNotificationBadge, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { SpanLinks } from '.'; +import { SpanLinksCount } from '../../app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers'; +import { ProcessorEvent } from '../../../../common/processor_event'; + +interface Props { + spanLinksCount: SpanLinksCount; + traceId: string; + spanId: string; + processorEvent: ProcessorEvent; +} + +export function getSpanLinksTabContent({ + spanLinksCount, + traceId, + spanId, + processorEvent, +}: Props) { + if (!spanLinksCount.linkedChildren && !spanLinksCount.linkedParents) { + return undefined; + } + + return { + id: 'span_links', + 'data-test-subj': 'spanLinksTab', + name: ( + <> + {i18n.translate('xpack.apm.propertiesTable.tabs.spanLinks', { + defaultMessage: 'Span links', + })} + + ), + append: ( + + {spanLinksCount.linkedChildren + spanLinksCount.linkedParents} + + ), + content: ( + <> + + + + ), + }; +} diff --git a/x-pack/plugins/apm/public/components/shared/span_links/span_links_table.tsx b/x-pack/plugins/apm/public/components/shared/span_links/span_links_table.tsx new file mode 100644 index 0000000000000..f14cfd3e086d7 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/span_links/span_links_table.tsx @@ -0,0 +1,234 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + EuiBasicTableColumn, + EuiButtonEmpty, + EuiButtonIcon, + EuiCopy, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiInMemoryTable, + EuiLink, + EuiPopover, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; +import { SpanLinkDetails } from '../../../../common/span_links'; +import { asDuration } from '../../../../common/utils/formatters'; +import { useApmParams } from '../../../hooks/use_apm_params'; +import { useApmRouter } from '../../../hooks/use_apm_router'; +import { ServiceLink } from '../service_link'; +import { getSpanIcon } from '../span_icon/get_span_icon'; + +interface Props { + items: SpanLinkDetails[]; +} + +export function SpanLinksTable({ items }: Props) { + const router = useApmRouter(); + const { + query: { rangeFrom, rangeTo, comparisonEnabled }, + } = useApmParams('/services/{serviceName}/transactions/view'); + const [idActionMenuOpen, setIdActionMenuOpen] = useState< + string | undefined + >(); + + const columns: Array> = [ + { + field: 'serviceName', + name: i18n.translate('xpack.apm.spanLinks.table.serviceName', { + defaultMessage: 'Service name', + }), + sortable: true, + render: (_, { details }) => { + if (details) { + return ( + + ); + } + return ( + + + + + + {i18n.translate('xpack.apm.spanLinks.table.serviceName.unknown', { + defaultMessage: 'Unknown', + })} + + + ); + }, + }, + { + field: 'spanId', + name: i18n.translate('xpack.apm.spanLinks.table.span', { + defaultMessage: 'Span', + }), + sortable: true, + render: (_, { spanId, traceId, details }) => { + if (details && details.transactionId) { + return ( + + + + + + + {details.spanName} + + + + ); + } + return `${traceId}-${spanId}`; + }, + }, + { + field: 'duration', + name: i18n.translate('xpack.apm.spanLinks.table.spanDuration', { + defaultMessage: 'Span duration', + }), + sortable: true, + width: '150', + render: (_, { details }) => { + return ( + + {asDuration(details?.duration)} + + ); + }, + }, + { + field: 'actions', + name: 'Actions', + width: '100', + render: (_, { spanId, traceId, details }) => { + const id = `${traceId}:${spanId}`; + return ( + { + setIdActionMenuOpen(id); + }} + /> + } + isOpen={idActionMenuOpen === id} + closePopover={() => { + setIdActionMenuOpen(undefined); + }} + > + + {details?.transactionId && ( + + + {i18n.translate( + 'xpack.apm.spanLinks.table.actions.goToTraceDetails', + { defaultMessage: 'Go to trace' } + )} + + + )} + + + {(copy) => ( + { + copy(); + setIdActionMenuOpen(undefined); + }} + flush="both" + > + {i18n.translate( + 'xpack.apm.spanLinks.table.actions.copyParentTraceId', + { defaultMessage: 'Copy parent trace id' } + )} + + )} + + + {details?.transactionId && ( + + + {i18n.translate( + 'xpack.apm.spanLinks.table.actions.goToSpanDetails', + { defaultMessage: 'Go to span details' } + )} + + + )} + + + {(copy) => ( + { + copy(); + setIdActionMenuOpen(undefined); + }} + flush="both" + > + {i18n.translate( + 'xpack.apm.spanLinks.table.actions.copySpanId', + { defaultMessage: 'Copy span id' } + )} + + )} + + + + + ); + }, + }, + ]; + + return ( + + ); +} diff --git a/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts index 5e6ac627364d8..7224a58fda982 100644 --- a/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts @@ -37,6 +37,7 @@ import { historicalDataRouteRepository } from '../historical_data/route'; import { eventMetadataRouteRepository } from '../event_metadata/route'; import { suggestionsRouteRepository } from '../suggestions/route'; import { agentKeysRouteRepository } from '../agent_keys/route'; +import { spanLinksRouteRepository } from '../span_links/route'; function getTypedGlobalApmServerRouteRepository() { const repository = { @@ -67,6 +68,7 @@ function getTypedGlobalApmServerRouteRepository() { ...historicalDataRouteRepository, ...eventMetadataRouteRepository, ...agentKeysRouteRepository, + ...spanLinksRouteRepository, }; return repository; diff --git a/x-pack/plugins/apm/server/routes/span_links/get_linked_children.ts b/x-pack/plugins/apm/server/routes/span_links/get_linked_children.ts new file mode 100644 index 0000000000000..43b55d31503e4 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/span_links/get_linked_children.ts @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { rangeQuery } from '@kbn/observability-plugin/server'; +import { isEmpty } from 'lodash'; +import { + PROCESSOR_EVENT, + SPAN_ID, + SPAN_LINKS, + SPAN_LINKS_TRACE_ID, + SPAN_LINKS_SPAN_ID, + TRACE_ID, + TRANSACTION_ID, +} from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; +import type { SpanRaw } from '../../../typings/es_schemas/raw/span_raw'; +import type { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw'; +import { Setup } from '../../lib/helpers/setup_request'; +import { getBufferedTimerange } from './utils'; + +async function fetchLinkedChildrenOfSpan({ + traceId, + setup, + start, + end, + spanId, +}: { + traceId: string; + setup: Setup; + start: number; + end: number; + spanId?: string; +}) { + const { apmEventClient } = setup; + + const { startWithBuffer, endWithBuffer } = getBufferedTimerange({ + start, + end, + }); + + const response = await apmEventClient.search( + 'fetch_linked_children_of_span', + { + apm: { + events: [ProcessorEvent.span, ProcessorEvent.transaction], + }, + _source: [SPAN_LINKS, TRACE_ID, SPAN_ID, PROCESSOR_EVENT, TRANSACTION_ID], + body: { + size: 1000, + query: { + bool: { + filter: [ + ...rangeQuery(startWithBuffer, endWithBuffer), + { term: { [SPAN_LINKS_TRACE_ID]: traceId } }, + ...(spanId ? [{ term: { [SPAN_LINKS_SPAN_ID]: spanId } }] : []), + ], + }, + }, + }, + } + ); + // Filter out documents that don't have any span.links that match the combination of traceId and spanId + return response.hits.hits.filter(({ _source: source }) => { + const spanLinks = source.span?.links?.filter((spanLink) => { + return ( + spanLink.trace.id === traceId && + (spanId ? spanLink.span.id === spanId : true) + ); + }); + return !isEmpty(spanLinks); + }); +} + +function getSpanId(source: TransactionRaw | SpanRaw) { + return source.processor.event === ProcessorEvent.span + ? (source as SpanRaw).span.id + : (source as TransactionRaw).transaction?.id; +} + +export async function getLinkedChildrenCountBySpanId({ + traceId, + setup, + start, + end, +}: { + traceId: string; + setup: Setup; + start: number; + end: number; +}) { + const linkedChildren = await fetchLinkedChildrenOfSpan({ + traceId, + setup, + start, + end, + }); + return linkedChildren.reduce>( + (acc, { _source: source }) => { + source.span?.links?.forEach((link) => { + // Ignores span links that don't belong to this trace + if (link.trace.id === traceId) { + acc[link.span.id] = (acc[link.span.id] || 0) + 1; + } + }); + return acc; + }, + {} + ); +} + +export async function getLinkedChildrenOfSpan({ + traceId, + spanId, + setup, + start, + end, +}: { + traceId: string; + spanId: string; + setup: Setup; + start: number; + end: number; +}) { + const linkedChildren = await fetchLinkedChildrenOfSpan({ + traceId, + spanId, + setup, + start, + end, + }); + + return linkedChildren.map(({ _source: source }) => { + return { + trace: { id: source.trace.id }, + span: { id: getSpanId(source) }, + }; + }); +} diff --git a/x-pack/plugins/apm/server/routes/span_links/get_linked_parents.ts b/x-pack/plugins/apm/server/routes/span_links/get_linked_parents.ts new file mode 100644 index 0000000000000..876a0f3718642 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/span_links/get_linked_parents.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { rangeQuery } from '@kbn/observability-plugin/server'; +import { + SPAN_ID, + SPAN_LINKS, + TRACE_ID, + TRANSACTION_ID, + PROCESSOR_EVENT, +} from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { SpanRaw } from '../../../typings/es_schemas/raw/span_raw'; +import { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw'; +import { Setup } from '../../lib/helpers/setup_request'; + +export async function getLinkedParentsOfSpan({ + setup, + traceId, + spanId, + start, + end, + processorEvent, +}: { + traceId: string; + spanId: string; + setup: Setup; + start: number; + end: number; + processorEvent: ProcessorEvent; +}) { + const { apmEventClient } = setup; + + const response = await apmEventClient.search('get_linked_parents_of_span', { + apm: { + events: [processorEvent], + }, + _source: [SPAN_LINKS], + body: { + size: 1, + query: { + bool: { + filter: [ + ...rangeQuery(start, end), + { term: { [TRACE_ID]: traceId } }, + { exists: { field: SPAN_LINKS } }, + { term: { [PROCESSOR_EVENT]: processorEvent } }, + ...(processorEvent === ProcessorEvent.transaction + ? [{ term: { [TRANSACTION_ID]: spanId } }] + : [{ term: { [SPAN_ID]: spanId } }]), + ], + }, + }, + }, + }); + + const source = response.hits.hits?.[0]?._source as TransactionRaw | SpanRaw; + + return source?.span?.links || []; +} diff --git a/x-pack/plugins/apm/server/routes/span_links/get_span_links_details.ts b/x-pack/plugins/apm/server/routes/span_links/get_span_links_details.ts new file mode 100644 index 0000000000000..cffd7ff826c88 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/span_links/get_span_links_details.ts @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server'; +import { chunk, compact, isEmpty, keyBy } from 'lodash'; +import { + SERVICE_NAME, + SPAN_ID, + SPAN_NAME, + TRACE_ID, + TRANSACTION_ID, + TRANSACTION_NAME, + TRANSACTION_DURATION, + SPAN_DURATION, + PROCESSOR_EVENT, + SPAN_SUBTYPE, + SPAN_TYPE, + AGENT_NAME, +} from '../../../common/elasticsearch_fieldnames'; +import { Environment } from '../../../common/environment_rt'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { SpanLinkDetails } from '../../../common/span_links'; +import { SpanLink } from '../../../typings/es_schemas/raw/fields/span_links'; +import { SpanRaw } from '../../../typings/es_schemas/raw/span_raw'; +import { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw'; +import { Setup } from '../../lib/helpers/setup_request'; +import { getBufferedTimerange } from './utils'; + +async function fetchSpanLinksDetails({ + setup, + kuery, + spanLinks, + start, + end, +}: { + setup: Setup; + kuery: string; + spanLinks: SpanLink[]; + start: number; + end: number; +}) { + const { apmEventClient } = setup; + + const { startWithBuffer, endWithBuffer } = getBufferedTimerange({ + start, + end, + }); + + const response = await apmEventClient.search('get_span_links_details', { + apm: { + events: [ProcessorEvent.span, ProcessorEvent.transaction], + }, + _source: [ + TRACE_ID, + SPAN_ID, + TRANSACTION_ID, + SERVICE_NAME, + SPAN_NAME, + TRANSACTION_NAME, + TRANSACTION_DURATION, + SPAN_DURATION, + PROCESSOR_EVENT, + SPAN_SUBTYPE, + SPAN_TYPE, + AGENT_NAME, + ], + body: { + size: 1000, + query: { + bool: { + filter: [ + ...rangeQuery(startWithBuffer, endWithBuffer), + ...kqlQuery(kuery), + { + bool: { + should: spanLinks.map((item) => { + return { + bool: { + filter: [ + { term: { [TRACE_ID]: item.trace.id } }, + { + bool: { + should: [ + { term: { [SPAN_ID]: item.span.id } }, + { term: { [TRANSACTION_ID]: item.span.id } }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }; + }), + minimum_should_match: 1, + }, + }, + ], + }, + }, + }, + }); + + const spanIdsMap = keyBy(spanLinks, 'span.id'); + + return response.hits.hits.filter(({ _source: source }) => { + // The above query might return other spans from the same transaction because siblings spans share the same transaction.id + // so, if it is a span we need to guarantee that the span.id is the same as the span links ids + if (source.processor.event === ProcessorEvent.span) { + const span = source as SpanRaw; + const hasSpanId = spanIdsMap[span.span.id] || false; + return hasSpanId; + } + return true; + }); +} + +export async function getSpanLinksDetails({ + setup, + spanLinks, + kuery, + start, + end, +}: { + setup: Setup; + spanLinks: SpanLink[]; + kuery: string; + start: number; + end: number; +}): Promise { + if (!spanLinks.length) { + return []; + } + + // chunk span links to avoid too_many_nested_clauses problem + const spanLinksChunks = chunk(spanLinks, 500); + const chunkedResponses = await Promise.all( + spanLinksChunks.map((spanLinksChunk) => + fetchSpanLinksDetails({ + setup, + kuery, + spanLinks: spanLinksChunk, + start, + end, + }) + ) + ); + + const linkedSpans = chunkedResponses.flat(); + + // Creates a map for all span links details found + const spanLinksDetailsMap = linkedSpans.reduce< + Record + >((acc, { _source: source }) => { + const commonDetails = { + serviceName: source.service.name, + agentName: source.agent.name, + environment: source.service.environment as Environment, + transactionId: source.transaction?.id, + }; + + if (source.processor.event === ProcessorEvent.transaction) { + const transaction = source as TransactionRaw; + const key = `${transaction.trace.id}:${transaction.transaction.id}`; + acc[key] = { + traceId: source.trace.id, + spanId: transaction.transaction.id, + details: { + ...commonDetails, + spanName: transaction.transaction.name, + duration: transaction.transaction.duration.us, + }, + }; + } else { + const span = source as SpanRaw; + const key = `${span.trace.id}:${span.span.id}`; + acc[key] = { + traceId: source.trace.id, + spanId: span.span.id, + details: { + ...commonDetails, + spanName: span.span.name, + duration: span.span.duration.us, + spanSubtype: span.span.subtype, + spanType: span.span.type, + }, + }; + } + + return acc; + }, {}); + + // It's important to keep the original order of the span links, + // so loops trough the original list merging external links and links with details. + // external links are links that the details were not found in the ES query. + return compact( + spanLinks.map((item) => { + const key = `${item.trace.id}:${item.span.id}`; + const details = spanLinksDetailsMap[key]; + if (details) { + return details; + } + + // When kuery is not set, returns external links, if not hides this item. + return isEmpty(kuery) + ? { traceId: item.trace.id, spanId: item.span.id } + : undefined; + }) + ); +} diff --git a/x-pack/plugins/apm/server/routes/span_links/route.ts b/x-pack/plugins/apm/server/routes/span_links/route.ts new file mode 100644 index 0000000000000..34b5864778144 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/span_links/route.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import * as t from 'io-ts'; +import { setupRequest } from '../../lib/helpers/setup_request'; +import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; +import { getSpanLinksDetails } from './get_span_links_details'; +import { getLinkedChildrenOfSpan } from './get_linked_children'; +import { kueryRt, rangeRt } from '../default_api_types'; +import { SpanLinkDetails } from '../../../common/span_links'; +import { processorEventRt } from '../../../common/processor_event'; +import { getLinkedParentsOfSpan } from './get_linked_parents'; + +const linkedParentsRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/traces/{traceId}/span_links/{spanId}/parents', + params: t.type({ + path: t.type({ + traceId: t.string, + spanId: t.string, + }), + query: t.intersection([ + kueryRt, + rangeRt, + t.type({ processorEvent: processorEventRt }), + ]), + }), + options: { tags: ['access:apm'] }, + handler: async ( + resources + ): Promise<{ + spanLinksDetails: SpanLinkDetails[]; + }> => { + const { + params: { query, path }, + } = resources; + const setup = await setupRequest(resources); + const linkedParents = await getLinkedParentsOfSpan({ + setup, + traceId: path.traceId, + spanId: path.spanId, + start: query.start, + end: query.end, + processorEvent: query.processorEvent, + }); + + return { + spanLinksDetails: await getSpanLinksDetails({ + setup, + spanLinks: linkedParents, + kuery: query.kuery, + start: query.start, + end: query.end, + }), + }; + }, +}); + +const linkedChildrenRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/traces/{traceId}/span_links/{spanId}/children', + params: t.type({ + path: t.type({ + traceId: t.string, + spanId: t.string, + }), + query: t.intersection([kueryRt, rangeRt]), + }), + options: { tags: ['access:apm'] }, + handler: async ( + resources + ): Promise<{ + spanLinksDetails: SpanLinkDetails[]; + }> => { + const { + params: { query, path }, + } = resources; + const setup = await setupRequest(resources); + const linkedChildren = await getLinkedChildrenOfSpan({ + setup, + traceId: path.traceId, + spanId: path.spanId, + start: query.start, + end: query.end, + }); + + return { + spanLinksDetails: await getSpanLinksDetails({ + setup, + spanLinks: linkedChildren, + kuery: query.kuery, + start: query.start, + end: query.end, + }), + }; + }, +}); + +export const spanLinksRouteRepository = { + ...linkedParentsRoute, + ...linkedChildrenRoute, +}; diff --git a/x-pack/plugins/apm/server/routes/span_links/utils.ts b/x-pack/plugins/apm/server/routes/span_links/utils.ts new file mode 100644 index 0000000000000..7425d1b774286 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/span_links/utils.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment'; + +export function getBufferedTimerange({ + start, + end, + bufferSize = 4, +}: { + start: number; + end: number; + bufferSize?: number; +}) { + return { + startWithBuffer: moment(start).subtract(bufferSize, 'days').valueOf(), + endWithBuffer: moment(end).add(bufferSize, 'days').valueOf(), + }; +} diff --git a/x-pack/plugins/apm/server/routes/traces/get_trace_items.ts b/x-pack/plugins/apm/server/routes/traces/get_trace_items.ts index d754c2d53b71f..3f6146c713303 100644 --- a/x-pack/plugins/apm/server/routes/traces/get_trace_items.ts +++ b/x-pack/plugins/apm/server/routes/traces/get_trace_items.ts @@ -10,15 +10,16 @@ import { Sort, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { rangeQuery } from '@kbn/observability-plugin/server'; -import { ProcessorEvent } from '../../../common/processor_event'; import { + ERROR_LOG_LEVEL, + PARENT_ID, + SPAN_DURATION, TRACE_ID, TRANSACTION_DURATION, - SPAN_DURATION, - PARENT_ID, - ERROR_LOG_LEVEL, } from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; import { Setup } from '../../lib/helpers/setup_request'; +import { getLinkedChildrenCountBySpanId } from '../span_links/get_linked_children'; export async function getTraceItems( traceId: string, @@ -74,12 +75,21 @@ export async function getTraceItems( }, }); - const errorResponse = await errorResponsePromise; - const traceResponse = await traceResponsePromise; + const [errorResponse, traceResponse, linkedChildrenOfSpanCountBySpanId] = + await Promise.all([ + errorResponsePromise, + traceResponsePromise, + getLinkedChildrenCountBySpanId({ traceId, setup, start, end }), + ]); const exceedsMax = traceResponse.hits.total.value > maxTraceItems; const traceDocs = traceResponse.hits.hits.map((hit) => hit._source); const errorDocs = errorResponse.hits.hits.map((hit) => hit._source); - return { exceedsMax, traceDocs, errorDocs }; + return { + exceedsMax, + traceDocs, + errorDocs, + linkedChildrenOfSpanCountBySpanId, + }; } diff --git a/x-pack/plugins/apm/server/routes/traces/route.ts b/x-pack/plugins/apm/server/routes/traces/route.ts index c767a4e67aa63..afca332fea0b5 100644 --- a/x-pack/plugins/apm/server/routes/traces/route.ts +++ b/x-pack/plugins/apm/server/routes/traces/route.ts @@ -82,6 +82,7 @@ const tracesByIdRoute = createApmServerRoute({ errorDocs: Array< import('./../../../typings/es_schemas/ui/apm_error').APMError >; + linkedChildrenOfSpanCountBySpanId: Record; }> => { const setup = await setupRequest(resources); const { params } = resources; diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/fields/span_links.ts b/x-pack/plugins/apm/typings/es_schemas/raw/fields/span_links.ts new file mode 100644 index 0000000000000..5e0028ad58176 --- /dev/null +++ b/x-pack/plugins/apm/typings/es_schemas/raw/fields/span_links.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface SpanLink { + trace: { id: string }; + span: { id: string }; +} diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts index d191cb6d4e84c..1cac68f74b8b7 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts @@ -8,6 +8,7 @@ import { APMBaseDoc } from './apm_base_doc'; import { EventOutcome } from './fields/event_outcome'; import { Http } from './fields/http'; +import { SpanLink } from './fields/span_links'; import { Stackframe } from './fields/stackframe'; import { TimestampUs } from './fields/timestamp_us'; import { Url } from './fields/url'; @@ -63,6 +64,7 @@ export interface SpanRaw extends APMBaseDoc { sum: { us: number }; compression_strategy: string; }; + links?: SpanLink[]; }; timestamp: TimestampUs; transaction?: { diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts index 0811bfb8c1a79..4046bb9470fb7 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/transaction_raw.ts @@ -20,6 +20,7 @@ import { Url } from './fields/url'; import { User } from './fields/user'; import { UserAgent } from './fields/user_agent'; import { Faas } from './fields/faas'; +import { SpanLink } from './fields/span_links'; interface Processor { name: 'transaction'; @@ -71,4 +72,7 @@ export interface TransactionRaw extends APMBaseDoc { user_agent?: UserAgent; cloud?: Cloud; faas?: Faas; + span?: { + links?: SpanLink[]; + }; } diff --git a/x-pack/plugins/cases/docs/openapi/bundled.json b/x-pack/plugins/cases/docs/openapi/bundled.json index 31feae3331b04..0cb084b5beb7c 100644 --- a/x-pack/plugins/cases/docs/openapi/bundled.json +++ b/x-pack/plugins/cases/docs/openapi/bundled.json @@ -341,6 +341,11 @@ "type": "string", "example": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" }, + "duration": { + "type": "integer", + "description": "The elapsed time from the creation of the case to its closure (in seconds). If the case has not been closed, the duration is set to null.", + "example": 120 + }, "external_service": { "type": "object", "properties": { @@ -821,6 +826,11 @@ "type": "string", "example": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" }, + "duration": { + "type": "integer", + "description": "The elapsed time from the creation of the case to its closure (in seconds). If the case has not been closed, the duration is set to null.", + "example": 120 + }, "external_service": { "type": "object", "properties": { @@ -1267,6 +1277,11 @@ "type": "string", "example": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" }, + "duration": { + "type": "integer", + "description": "The elapsed time from the creation of the case to its closure (in seconds). If the case has not been closed, the duration is set to null.", + "example": 120 + }, "external_service": { "type": "object", "properties": { @@ -1753,6 +1768,11 @@ "type": "string", "example": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" }, + "duration": { + "type": "integer", + "description": "The elapsed time from the creation of the case to its closure (in seconds). If the case has not been closed, the duration is set to null.", + "example": 120 + }, "external_service": { "type": "object", "properties": { @@ -1994,6 +2014,7 @@ }, "owner": "securitySolution", "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active", + "duration": null, "closed_at": null, "closed_by": null, "created_at": "2022-05-13T09:16:17.416Z", @@ -2068,6 +2089,7 @@ }, "owner": "securitySolution", "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active!", + "duration": null, "closed_at": null, "closed_by": null, "created_at": "2022-05-13T09:16:17.416Z", diff --git a/x-pack/plugins/cases/docs/openapi/bundled.yaml b/x-pack/plugins/cases/docs/openapi/bundled.yaml index afad92f489a74..083aef3c25ad2 100644 --- a/x-pack/plugins/cases/docs/openapi/bundled.yaml +++ b/x-pack/plugins/cases/docs/openapi/bundled.yaml @@ -318,6 +318,13 @@ paths: advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active + duration: + type: integer + description: >- + The elapsed time from the creation of the case to its + closure (in seconds). If the case has not been closed, the + duration is set to null. + example: 120 external_service: type: object properties: @@ -732,6 +739,13 @@ paths: advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active + duration: + type: integer + description: >- + The elapsed time from the creation of the case to its + closure (in seconds). If the case has not been closed, the + duration is set to null. + example: 120 external_service: type: object properties: @@ -1117,6 +1131,13 @@ paths: advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active + duration: + type: integer + description: >- + The elapsed time from the creation of the case to its + closure (in seconds). If the case has not been closed, the + duration is set to null. + example: 120 external_service: type: object properties: @@ -1533,6 +1554,13 @@ paths: advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active + duration: + type: integer + description: >- + The elapsed time from the creation of the case to its + closure (in seconds). If the case has not been closed, the + duration is set to null. + example: 120 external_service: type: object properties: @@ -1709,6 +1737,7 @@ components: James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active + duration: null closed_at: null closed_by: null created_at: '2022-05-13T09:16:17.416Z' @@ -1774,6 +1803,7 @@ components: James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active! + duration: null closed_at: null closed_by: null created_at: '2022-05-13T09:16:17.416Z' diff --git a/x-pack/plugins/cases/docs/openapi/components/examples/create_case_response.yaml b/x-pack/plugins/cases/docs/openapi/components/examples/create_case_response.yaml index f9f2ce3d61beb..bc5fa1f5bc049 100644 --- a/x-pack/plugins/cases/docs/openapi/components/examples/create_case_response.yaml +++ b/x-pack/plugins/cases/docs/openapi/components/examples/create_case_response.yaml @@ -17,6 +17,7 @@ value: }, "owner": "securitySolution", "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active", + "duration": null, "closed_at": null, "closed_by": null, "created_at": "2022-05-13T09:16:17.416Z", diff --git a/x-pack/plugins/cases/docs/openapi/components/examples/update_case_response.yaml b/x-pack/plugins/cases/docs/openapi/components/examples/update_case_response.yaml index a73191868c8ee..114669b893651 100644 --- a/x-pack/plugins/cases/docs/openapi/components/examples/update_case_response.yaml +++ b/x-pack/plugins/cases/docs/openapi/components/examples/update_case_response.yaml @@ -18,6 +18,7 @@ value: }, "owner": "securitySolution", "description": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active!", + "duration": null, "closed_at": null, "closed_by": null, "created_at": "2022-05-13T09:16:17.416Z", diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/case_response_properties.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/case_response_properties.yaml index 780496f1591b4..6a2c3c3963c3c 100644 --- a/x-pack/plugins/cases/docs/openapi/components/schemas/case_response_properties.yaml +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/case_response_properties.yaml @@ -42,6 +42,10 @@ created_by: description: type: string example: "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" +duration: + type: integer + description: The elapsed time from the creation of the case to its closure (in seconds). If the case has not been closed, the duration is set to null. + example: 120 external_service: type: object properties: diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/steps/install_fleet_server.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/steps/install_fleet_server.tsx index 7eb9c5cbd38bb..fe84d933c7495 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/steps/install_fleet_server.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/fleet_server_instructions/steps/install_fleet_server.tsx @@ -8,12 +8,12 @@ import React from 'react'; import type { EuiStepProps } from '@elastic/eui'; -import { EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import type { PLATFORM_TYPE } from '../../../hooks'; -import { useDefaultOutput, useKibanaVersion } from '../../../hooks'; +import { useStartServices, useDefaultOutput, useKibanaVersion } from '../../../hooks'; import { PlatformSelector } from '../..'; @@ -58,6 +58,7 @@ const InstallFleetServerStepContent: React.FunctionComponent<{ fleetServerPolicyId?: string; deploymentMode: DeploymentMode; }> = ({ serviceToken, fleetServerHost, fleetServerPolicyId, deploymentMode }) => { + const { docLinks } = useStartServices(); const kibanaVersion = useKibanaVersion(); const { output } = useDefaultOutput(); @@ -84,7 +85,17 @@ const InstallFleetServerStepContent: React.FunctionComponent<{ + + + ), + }} /> diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/installation_message.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/installation_message.tsx index 81404df2eeabd..48531ef166714 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/installation_message.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/installation_message.tsx @@ -12,7 +12,7 @@ import semverMajor from 'semver/functions/major'; import semverMinor from 'semver/functions/minor'; import semverPatch from 'semver/functions/patch'; -import { useKibanaVersion } from '../../hooks'; +import { useKibanaVersion, useStartServices } from '../../hooks'; import type { K8sMode } from './types'; @@ -21,6 +21,7 @@ interface Props { } export const InstallationMessage: React.FunctionComponent = ({ isK8s }) => { + const { docLinks } = useStartServices(); const kibanaVersion = useKibanaVersion(); const kibanaVersionURLString = useMemo( () => @@ -54,11 +55,7 @@ export const InstallationMessage: React.FunctionComponent = ({ isK8s }) = ), installationLink: ( - + ; + }>; +} + +export interface FormattedQuestionAnsweringResult { + value: string; + predictionProbability: number; + startOffset: number; + endOffset: number; +} + +export type FormattedQuestionAnsweringResponse = FormattedQuestionAnsweringResult[]; + +export type QuestionAnsweringResponse = InferResponse< + FormattedQuestionAnsweringResponse, + RawQuestionAnsweringResponse +>; + +export class QuestionAnsweringInference extends InferenceBase { + protected inferenceType = SUPPORTED_PYTORCH_TASKS.QUESTION_ANSWERING; + + public questionText$ = new BehaviorSubject(''); + + public async infer() { + try { + this.setRunning(); + const inputText = this.inputText$.getValue(); + const questionText = this.questionText$.value; + const numTopClassesConfig = this.getNumTopClassesConfig()?.inference_config; + + const payload = { + docs: [{ [this.inputField]: inputText }], + inference_config: { + [this.inferenceType]: { + question: questionText, + ...(numTopClassesConfig + ? { + num_top_classes: numTopClassesConfig[this.inferenceType].num_top_classes, + } + : {}), + }, + }, + }; + const resp = (await this.trainedModelsApi.inferTrainedModel( + this.model.model_id, + payload, + '30s' + )) as unknown as RawQuestionAnsweringResponse; + + const processedResponse: QuestionAnsweringResponse = processResponse( + resp, + this.model, + inputText + ); + + this.inferenceResult$.next(processedResponse); + this.setFinished(); + + return processedResponse; + } catch (error) { + this.setFinishedWithErrors(error); + throw error; + } + } + + public getInputComponent(): JSX.Element { + return getQuestionAnsweringInput(this); + } + + public getOutputComponent(): JSX.Element { + return getQuestionAnsweringOutputComponent(this); + } +} + +function processResponse( + resp: RawQuestionAnsweringResponse, + model: estypes.MlTrainedModelConfig, + inputText: string +) { + const { + inference_results: [inferenceResults], + } = resp; + + let formattedResponse = [ + { + value: inferenceResults.predicted_value, + predictionProbability: inferenceResults.prediction_probability, + startOffset: inferenceResults.start_offset, + endOffset: inferenceResults.end_offset, + }, + ]; + + if (inferenceResults.top_classes !== undefined) { + formattedResponse = inferenceResults.top_classes.map((topClass) => { + return { + value: topClass.answer, + predictionProbability: topClass.score, + startOffset: topClass.start_offset, + endOffset: topClass.end_offset, + }; + }); + } + + return { + response: formattedResponse, + rawResponse: resp, + inputText, + }; +} diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/question_answering/question_answering_input.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/question_answering/question_answering_input.tsx new file mode 100644 index 0000000000000..fffff1b382c76 --- /dev/null +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/question_answering/question_answering_input.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useEffect, useState } from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { i18n } from '@kbn/i18n'; + +import { EuiSpacer, EuiFieldText, EuiFormRow } from '@elastic/eui'; + +import { TextInput } from '../text_input'; +import { QuestionAnsweringInference } from './question_answering_inference'; +import { RUNNING_STATE } from '../inference_base'; + +const QuestionInput: FC<{ + inferrer: QuestionAnsweringInference; +}> = ({ inferrer }) => { + const [questionText, setQuestionText] = useState(''); + + useEffect(() => { + inferrer.questionText$.next(questionText); + }, [questionText]); + + const runningState = useObservable(inferrer.runningState$); + return ( + + { + setQuestionText(e.target.value); + }} + /> + + ); +}; + +export const getQuestionAnsweringInput = ( + inferrer: QuestionAnsweringInference, + placeholder?: string +) => ( + <> + + + + +); diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/question_answering/question_answering_output.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/question_answering/question_answering_output.tsx new file mode 100644 index 0000000000000..95c1533efd709 --- /dev/null +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/models/question_answering/question_answering_output.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, ReactNode } from 'react'; +import useObservable from 'react-use/lib/useObservable'; + +import { EuiBadge } from '@elastic/eui'; + +import { useCurrentEuiTheme } from '../../../../../components/color_range_legend/use_color_range'; + +import type { + QuestionAnsweringInference, + FormattedQuestionAnsweringResult, +} from './question_answering_inference'; + +const ICON_PADDING = '2px'; +const TRIM_CHAR_COUNT = 200; + +export const getQuestionAnsweringOutputComponent = (inferrer: QuestionAnsweringInference) => ( + +); + +const QuestionAnsweringOutput: FC<{ inferrer: QuestionAnsweringInference }> = ({ inferrer }) => { + const result = useObservable(inferrer.inferenceResult$); + if (!result || result.response.length === 0) { + return null; + } + + const bestResult = result.response[0]; + const { inputText } = result; + + return <>{insertHighlighting(bestResult, inputText)}; +}; + +function insertHighlighting(result: FormattedQuestionAnsweringResult, inputText: string) { + const start = inputText.slice(0, result.startOffset); + const end = inputText.slice(result.endOffset, inputText.length); + const truncatedStart = + start.length > TRIM_CHAR_COUNT + ? `...${start.slice(start.length - TRIM_CHAR_COUNT, start.length)}` + : start; + const truncatedEnd = end.length > TRIM_CHAR_COUNT ? `${end.slice(0, TRIM_CHAR_COUNT)}...` : end; + + return ( +
+ {truncatedStart} + {result.value} + {truncatedEnd} +
+ ); +} + +const ResultBadge = ({ children }: { children: ReactNode }) => { + const { euiTheme } = useCurrentEuiTheme(); + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/selected_model.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/selected_model.tsx index 816166c5cbcbf..35f0670646004 100644 --- a/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/selected_model.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/test_models/selected_model.tsx @@ -9,6 +9,7 @@ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import React, { FC } from 'react'; import { NerInference } from './models/ner'; +import { QuestionAnsweringInference } from './models/question_answering'; import { TextClassificationInference, @@ -64,6 +65,11 @@ export const SelectedModel: FC = ({ model }) => { const inferrer = new FillMaskInference(trainedModels, model); return ; } + + if (Object.keys(model.inference_config)[0] === SUPPORTED_PYTORCH_TASKS.QUESTION_ANSWERING) { + const inferrer = new QuestionAnsweringInference(trainedModels, model); + return ; + } } if (model.model_type === TRAINED_MODEL_TYPE.LANG_IDENT) { const inferrer = new LangIdentInference(trainedModels, model); diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts index 4df376acb256e..460264e776838 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts @@ -19,7 +19,7 @@ export interface EventSource { } export interface EventsActionGroupData { - key: number; + key: number | string; events: { bucket: EventsMatrixHistogramData[]; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.test.tsx index b459f13a85480..4aa68e524f846 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.test.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { showInitialLoadingSpinner } from './helpers'; +import { formatAlertsData, showInitialLoadingSpinner } from './helpers'; +import { result, textResult, stackedByBooleanField, stackedByTextField } from './mock_data'; describe('helpers', () => { describe('showInitialLoadingSpinner', () => { @@ -34,3 +35,15 @@ describe('helpers', () => { }); }); }); + +describe('formatAlertsData', () => { + test('stack by a boolean field', () => { + const res = formatAlertsData(stackedByBooleanField); + expect(res).toEqual(result); + }); + + test('stack by a text field', () => { + const res = formatAlertsData(stackedByTextField); + expect(res).toEqual(textResult); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.tsx index 1f5c67c61b9e2..a20a3e1c37e83 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/helpers.tsx @@ -17,19 +17,22 @@ const EMPTY_ALERTS_DATA: HistogramData[] = []; export const formatAlertsData = (alertsData: AlertSearchResponse<{}, AlertsAggregation> | null) => { const groupBuckets: AlertsGroupBucket[] = alertsData?.aggregations?.alertsByGrouping?.buckets ?? []; - return groupBuckets.reduce((acc, { key: group, alerts }) => { - const alertsBucket: AlertsBucket[] = alerts.buckets ?? []; + return groupBuckets.reduce( + (acc, { key_as_string: keyAsString, key: group, alerts }) => { + const alertsBucket: AlertsBucket[] = alerts.buckets ?? []; - return [ - ...acc, - // eslint-disable-next-line @typescript-eslint/naming-convention - ...alertsBucket.map(({ key, doc_count }: AlertsBucket) => ({ - x: key, - y: doc_count, - g: group, - })), - ]; - }, EMPTY_ALERTS_DATA); + return [ + ...acc, + // eslint-disable-next-line @typescript-eslint/naming-convention + ...alertsBucket.map(({ key, doc_count }: AlertsBucket) => ({ + x: key, + y: doc_count, + g: keyAsString ?? group.toString(), + })), + ]; + }, + EMPTY_ALERTS_DATA + ); }; export const getAlertsHistogramQuery = ( diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx index 297027fdb9ab6..63d3bbeed6785 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx @@ -186,7 +186,7 @@ export const AlertsHistogramPanel = memo( ), field: selectedStackByOption, timelineId, - value: bucket.key, + value: bucket?.key_as_string ?? bucket.key, })) : NO_LEGEND_DATA, [ diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/mock_data.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/mock_data.ts new file mode 100644 index 0000000000000..6e5551eb69201 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/mock_data.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export const stackedByBooleanField = { + took: 1, + timed_out: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { + total: { + value: 3, + relation: 'eq', + }, + hits: [], + }, + timeout: false, + aggregations: { + alertsByGrouping: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 1, + key_as_string: 'true', + doc_count: 2683, + alerts: { + buckets: [ + { key_as_string: '2022-05-10T15:34:48.075Z', key: 1652196888075, doc_count: 0 }, + { key_as_string: '2022-05-10T16:19:48.074Z', key: 1652199588074, doc_count: 0 }, + { key_as_string: '2022-05-10T17:04:48.073Z', key: 1652202288073, doc_count: 0 }, + ], + }, + }, + ], + }, + }, +}; + +export const result = [ + { x: 1652196888075, y: 0, g: 'true' }, + { x: 1652199588074, y: 0, g: 'true' }, + { x: 1652202288073, y: 0, g: 'true' }, +]; + +export const stackedByTextField = { + took: 1, + timeout: false, + _shards: { total: 1, successful: 1, skipped: 0, failed: 0 }, + hits: { + total: { + value: 3, + relation: 'eq', + }, + hits: [], + }, + aggregations: { + alertsByGrouping: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'MacBook-Pro.local', + doc_count: 2706, + alerts: { + buckets: [ + { key_as_string: '2022-05-10T15:34:48.075Z', key: 1652196888075, doc_count: 0 }, + { key_as_string: '2022-05-10T16:19:48.074Z', key: 1652199588074, doc_count: 0 }, + { key_as_string: '2022-05-10T17:04:48.073Z', key: 1652202288073, doc_count: 0 }, + ], + }, + }, + ], + }, + }, +}; + +export const textResult = [ + { x: 1652196888075, y: 0, g: 'MacBook-Pro.local' }, + { x: 1652199588074, y: 0, g: 'MacBook-Pro.local' }, + { x: 1652202288073, y: 0, g: 'MacBook-Pro.local' }, +]; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/types.ts index 8c2a53dc23d43..433fee1716a47 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/types.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/types.ts @@ -26,7 +26,8 @@ export interface AlertsBucket { } export interface AlertsGroupBucket { - key: string; + key: string | number; + key_as_string?: string; alerts: { buckets: AlertsBucket[]; }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/helpers.test.ts new file mode 100644 index 0000000000000..2680b604c6e28 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/helpers.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MatrixHistogramType } from '../../../../../common/search_strategy'; +import { getGenericData } from './helpers'; +import { stackedByBooleanField, stackedByTextField, result, textResult } from './mock_data'; + +describe('getGenericData', () => { + test('stack by a boolean field', () => { + const res = getGenericData(stackedByBooleanField, 'events.bucket'); + expect(res).toEqual(result); + }); + + test('stack by a text field', () => { + const res = getGenericData(stackedByTextField, 'events.bucket'); + expect(res).toEqual(textResult); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/helpers.ts index aa6b85d795443..73b90e7cc32ee 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/helpers.ts @@ -18,7 +18,8 @@ export const getGenericData = ( ): MatrixHistogramData[] => { let result: MatrixHistogramData[] = []; data.forEach((bucketData: unknown) => { - const group = get('key', bucketData); + // if key_as_string is present use it, else default to the existing key + const group = get('key_as_string', bucketData) ?? get('key', bucketData); const histData = getOr([], keyBucket, bucketData).map( // eslint-disable-next-line @typescript-eslint/naming-convention ({ key, doc_count }: MatrixHistogramBucket) => ({ diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/mock_data.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/mock_data.ts new file mode 100644 index 0000000000000..9a938846826a1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/mock_data.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const stackedByBooleanField = [ + { + key: 1, + key_as_string: 'true', + doc_count: 7125, + events: { + bucket: [ + { key_as_string: '2022-05-10T15:34:48.075Z', key: 1652196888075, doc_count: 0 }, + { key_as_string: '2022-05-10T16:19:48.074Z', key: 1652199588074, doc_count: 774 }, + { key_as_string: '2022-05-10T17:04:48.073Z', key: 1652202288073, doc_count: 415 }, + ], + }, + }, +]; +export const result = [ + { x: 1652196888075, y: 0, g: 'true' }, + { x: 1652199588074, y: 774, g: 'true' }, + { x: 1652202288073, y: 415, g: 'true' }, +]; + +export const stackedByTextField = [ + { + key: 'MacBook-Pro.local', + doc_count: 7103, + events: { + bucket: [ + { key_as_string: '2022-05-10T15:34:48.075Z', key: 1652196888075, doc_count: 0 }, + { key_as_string: '2022-05-10T16:19:48.074Z', key: 1652199588074, doc_count: 774 }, + { key_as_string: '2022-05-10T17:04:48.073Z', key: 1652202288073, doc_count: 415 }, + ], + }, + }, +]; + +export const textResult = [ + { x: 1652196888075, y: 0, g: 'MacBook-Pro.local' }, + { x: 1652199588074, y: 774, g: 'MacBook-Pro.local' }, + { x: 1652202288073, y: 415, g: 'MacBook-Pro.local' }, +]; diff --git a/x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_mappings_only_8.0.0/mappings.json b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_mappings_only_8.0.0/mappings.json index 2d05717fa5725..3167ad3f5a6a0 100644 --- a/x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_mappings_only_8.0.0/mappings.json +++ b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_mappings_only_8.0.0/mappings.json @@ -628,8 +628,7 @@ { "type": "index", "value": { - "aliases": { - }, + "aliases": {}, "index": ".ml-config", "mappings": { "_meta": { @@ -15510,6 +15509,26 @@ "type": { "ignore_above": 1024, "type": "keyword" + }, + "links": { + "properties": { + "span": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "trace": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } } } }, @@ -20620,6 +20639,26 @@ "type": { "ignore_above": 1024, "type": "keyword" + }, + "links": { + "properties": { + "span": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "trace": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } } } }, diff --git a/x-pack/test/apm_api_integration/tests/span_links/data_generator.ts b/x-pack/test/apm_api_integration/tests/span_links/data_generator.ts new file mode 100644 index 0000000000000..e9e9e5cffaa53 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/span_links/data_generator.ts @@ -0,0 +1,300 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { apm, timerange } from '@elastic/apm-synthtrace'; +import { SpanLink } from '@kbn/apm-plugin/typings/es_schemas/raw/fields/span_links'; +import uuid from 'uuid'; + +function getProducerInternalOnly() { + const producerInternalOnlyInstance = apm + .service('producer-internal-only', 'production', 'go') + .instance('instance a'); + + const events = timerange( + new Date('2022-01-01T00:00:00.000Z'), + new Date('2022-01-01T00:01:00.000Z') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return producerInternalOnlyInstance + .transaction(`Transaction A`) + .timestamp(timestamp) + .duration(1000) + .success() + .children( + producerInternalOnlyInstance + .span(`Span A`, 'external', 'http') + .timestamp(timestamp + 50) + .duration(100) + .success() + ); + }); + + const apmFields = events.toArray(); + const transactionA = apmFields.find((item) => item['processor.event'] === 'transaction'); + const spanA = apmFields.find((item) => item['processor.event'] === 'span'); + + const ids = { + transactionAId: transactionA?.['transaction.id']!, + traceId: spanA?.['trace.id']!, + spanAId: spanA?.['span.id']!, + }; + const spanASpanLink = { + trace: { id: spanA?.['trace.id']! }, + span: { id: spanA?.['span.id']! }, + }; + + return { + ids, + spanASpanLink, + apmFields, + }; +} + +function getProducerExternalOnly() { + const producerExternalOnlyInstance = apm + .service('producer-external-only', 'production', 'java') + .instance('instance b'); + + const events = timerange( + new Date('2022-01-01T00:02:00.000Z'), + new Date('2022-01-01T00:03:00.000Z') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return producerExternalOnlyInstance + .transaction(`Transaction B`) + .timestamp(timestamp) + .duration(1000) + .success() + .children( + producerExternalOnlyInstance + .span(`Span B`, 'external', 'http') + .defaults({ + 'span.links': [{ trace: { id: 'trace#1' }, span: { id: 'span#1' } }], + }) + .timestamp(timestamp + 50) + .duration(100) + .success(), + producerExternalOnlyInstance + .span(`Span B.1`, 'external', 'http') + .timestamp(timestamp + 50) + .duration(100) + .success() + ); + }); + + const apmFields = events.toArray(); + const transactionB = apmFields.find((item) => item['processor.event'] === 'transaction'); + const spanB = apmFields.find( + (item) => item['processor.event'] === 'span' && item['span.name'] === 'Span B' + ); + const ids = { + traceId: spanB?.['trace.id']!, + transactionBId: transactionB?.['transaction.id']!, + spanBId: spanB?.['span.id']!, + }; + + const spanBSpanLink = { + trace: { id: spanB?.['trace.id']! }, + span: { id: spanB?.['span.id']! }, + }; + + const transactionBSpanLink = { + trace: { id: transactionB?.['trace.id']! }, + span: { id: transactionB?.['transaction.id']! }, + }; + + return { + ids, + spanBSpanLink, + transactionBSpanLink, + apmFields, + }; +} + +function getProducerConsumer({ + producerInternalOnlySpanASpanLink, + producerExternalOnlySpanBLink, + producerExternalOnlyTransactionBLink, +}: { + producerInternalOnlySpanASpanLink: SpanLink; + producerExternalOnlySpanBLink: SpanLink; + producerExternalOnlyTransactionBLink: SpanLink; +}) { + const externalTraceId = uuid.v4(); + + const producerConsumerInstance = apm + .service('producer-consumer', 'production', 'ruby') + .instance('instance c'); + + const events = timerange( + new Date('2022-01-01T00:04:00.000Z'), + new Date('2022-01-01T00:05:00.000Z') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return producerConsumerInstance + .transaction(`Transaction C`) + .defaults({ + 'span.links': [ + producerInternalOnlySpanASpanLink, + producerExternalOnlyTransactionBLink, + { trace: { id: externalTraceId }, span: { id: producerExternalOnlySpanBLink.span.id } }, + ], + }) + .timestamp(timestamp) + .duration(1000) + .success() + .children( + producerConsumerInstance + .span(`Span C`, 'external', 'http') + .timestamp(timestamp + 50) + .duration(100) + .success() + ); + }); + + const apmFields = events.toArray(); + const transactionC = apmFields.find((item) => item['processor.event'] === 'transaction'); + const transactionCSpanLink = { + trace: { id: transactionC?.['trace.id']! }, + span: { id: transactionC?.['transaction.id']! }, + }; + const spanC = apmFields.find( + (item) => item['processor.event'] === 'span' || item['span.name'] === 'Span C' + ); + const spanCSpanLink = { + trace: { id: spanC?.['trace.id']! }, + span: { id: spanC?.['span.id']! }, + }; + const ids = { + traceId: transactionC?.['trace.id']!, + transactionCId: transactionC?.['transaction.id']!, + spanCId: spanC?.['span.id']!, + externalTraceId, + }; + return { + transactionCSpanLink, + spanCSpanLink, + ids, + apmFields, + }; +} + +function getConsumerMultiple({ + producerInternalOnlySpanALink, + producerExternalOnlySpanBLink, + producerConsumerSpanCLink, + producerConsumerTransactionCLink, +}: { + producerInternalOnlySpanALink: SpanLink; + producerExternalOnlySpanBLink: SpanLink; + producerConsumerSpanCLink: SpanLink; + producerConsumerTransactionCLink: SpanLink; +}) { + const consumerMultipleInstance = apm + .service('consumer-multiple', 'production', 'nodejs') + .instance('instance d'); + + const events = timerange( + new Date('2022-01-01T00:06:00.000Z'), + new Date('2022-01-01T00:07:00.000Z') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return consumerMultipleInstance + .transaction(`Transaction D`) + .defaults({ 'span.links': [producerInternalOnlySpanALink, producerConsumerSpanCLink] }) + .timestamp(timestamp) + .duration(1000) + .success() + .children( + consumerMultipleInstance + .span(`Span E`, 'external', 'http') + .defaults({ + 'span.links': [producerExternalOnlySpanBLink, producerConsumerTransactionCLink], + }) + .timestamp(timestamp + 50) + .duration(100) + .success() + ); + }); + const apmFields = events.toArray(); + const transactionD = apmFields.find((item) => item['processor.event'] === 'transaction'); + const spanE = apmFields.find((item) => item['processor.event'] === 'span'); + + const ids = { + traceId: transactionD?.['trace.id']!, + transactionDId: transactionD?.['transaction.id']!, + spanEId: spanE?.['span.id']!, + }; + + return { + ids, + apmFields, + }; +} + +/** + * Data ingestion summary: + * + * producer-internal-only (go) + * --Transaction A + * ----Span A + * + * producer-external-only (java) + * --Transaction B + * ----Span B + * ------span.links=external link + * ----Span B1 + * + * producer-consumer (ruby) + * --Transaction C + * ------span.links=Service A / Span A + * ------span.links=Service B / Transaction B + * ------span.links=External ID / Span B + * ----Span C + * + * consumer-multiple (nodejs) + * --Transaction D + * ------span.links= Service C / Span C | Service A / Span A + * ----Span E + * ------span.links= Service B / Span B | Service C / Transaction C + */ +export function generateSpanLinksData() { + const producerInternalOnly = getProducerInternalOnly(); + const producerExternalOnly = getProducerExternalOnly(); + const producerConsumer = getProducerConsumer({ + producerInternalOnlySpanASpanLink: producerInternalOnly.spanASpanLink, + producerExternalOnlySpanBLink: producerExternalOnly.spanBSpanLink, + producerExternalOnlyTransactionBLink: producerExternalOnly.transactionBSpanLink, + }); + const producerMultiple = getConsumerMultiple({ + producerInternalOnlySpanALink: producerInternalOnly.spanASpanLink, + producerExternalOnlySpanBLink: producerExternalOnly.spanBSpanLink, + producerConsumerSpanCLink: producerConsumer.spanCSpanLink, + producerConsumerTransactionCLink: producerConsumer.transactionCSpanLink, + }); + return { + apmFields: { + producerInternalOnly: producerInternalOnly.apmFields, + producerExternalOnly: producerExternalOnly.apmFields, + producerConsumer: producerConsumer.apmFields, + producerMultiple: producerMultiple.apmFields, + }, + ids: { + producerInternalOnly: producerInternalOnly.ids, + producerExternalOnly: producerExternalOnly.ids, + producerConsumer: producerConsumer.ids, + producerMultiple: producerMultiple.ids, + }, + }; +} diff --git a/x-pack/test/apm_api_integration/tests/span_links/span_links.spec.ts b/x-pack/test/apm_api_integration/tests/span_links/span_links.spec.ts new file mode 100644 index 0000000000000..e42c9e0fb00cd --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/span_links/span_links.spec.ts @@ -0,0 +1,496 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EntityArrayIterable } from '@elastic/apm-synthtrace'; +import { ProcessorEvent } from '@kbn/apm-plugin/common/processor_event'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { generateSpanLinksData } from './data_generator'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const registry = getService('registry'); + const apmApiClient = getService('apmApiClient'); + const synthtraceEsClient = getService('synthtraceEsClient'); + + const start = new Date('2022-01-01T00:00:00.000Z').getTime(); + const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; + + registry.when( + 'contains linked children', + { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, + () => { + let ids: ReturnType['ids']; + + before(async () => { + const spanLinksData = generateSpanLinksData(); + + ids = spanLinksData.ids; + + await synthtraceEsClient.index( + new EntityArrayIterable(spanLinksData.apmFields.producerInternalOnly).merge( + new EntityArrayIterable(spanLinksData.apmFields.producerExternalOnly), + new EntityArrayIterable(spanLinksData.apmFields.producerConsumer), + new EntityArrayIterable(spanLinksData.apmFields.producerMultiple) + ) + ); + }); + + after(() => synthtraceEsClient.clean()); + + describe('Span links count on traces', () => { + async function fetchTraces({ traceId }: { traceId: string }) { + return await apmApiClient.readUser({ + endpoint: `GET /internal/apm/traces/{traceId}`, + params: { + path: { traceId }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + }, + }, + }); + } + + describe('producer-internal-only trace', () => { + let traces: Awaited>['body']; + before(async () => { + const tracesResponse = await fetchTraces({ traceId: ids.producerInternalOnly.traceId }); + traces = tracesResponse.body; + }); + + it('contains two children link on Span A', () => { + expect(Object.values(traces.linkedChildrenOfSpanCountBySpanId).length).to.equal(1); + expect( + traces.linkedChildrenOfSpanCountBySpanId[ids.producerInternalOnly.spanAId] + ).to.equal(2); + }); + }); + + describe('producer-external-only trace', () => { + let traces: Awaited>['body']; + before(async () => { + const tracesResponse = await fetchTraces({ traceId: ids.producerExternalOnly.traceId }); + traces = tracesResponse.body; + }); + + it('contains two children link on Span B', () => { + expect(Object.values(traces.linkedChildrenOfSpanCountBySpanId).length).to.equal(2); + expect( + traces.linkedChildrenOfSpanCountBySpanId[ids.producerExternalOnly.spanBId] + ).to.equal(1); + expect( + traces.linkedChildrenOfSpanCountBySpanId[ids.producerExternalOnly.transactionBId] + ).to.equal(1); + }); + }); + + describe('producer-consumer trace', () => { + let traces: Awaited>['body']; + before(async () => { + const tracesResponse = await fetchTraces({ traceId: ids.producerConsumer.traceId }); + traces = tracesResponse.body; + }); + + it('contains one children link on transaction C and two on span C', () => { + expect(Object.values(traces.linkedChildrenOfSpanCountBySpanId).length).to.equal(2); + expect( + traces.linkedChildrenOfSpanCountBySpanId[ids.producerConsumer.transactionCId] + ).to.equal(1); + expect(traces.linkedChildrenOfSpanCountBySpanId[ids.producerConsumer.spanCId]).to.equal( + 1 + ); + }); + }); + + describe('consumer-multiple trace', () => { + let traces: Awaited>['body']; + before(async () => { + const tracesResponse = await fetchTraces({ traceId: ids.producerMultiple.traceId }); + traces = tracesResponse.body; + }); + + it('contains no children', () => { + expect(Object.values(traces.linkedChildrenOfSpanCountBySpanId).length).to.equal(0); + expect( + traces.linkedChildrenOfSpanCountBySpanId[ids.producerMultiple.transactionDId] + ).to.equal(undefined); + expect(traces.linkedChildrenOfSpanCountBySpanId[ids.producerMultiple.spanEId]).to.equal( + undefined + ); + }); + }); + }); + + describe('Span links details', () => { + async function fetchChildrenAndParentsDetails({ + kuery, + traceId, + spanId, + processorEvent, + }: { + kuery: string; + traceId: string; + spanId: string; + processorEvent: ProcessorEvent; + }) { + const [childrenLinksResponse, parentsLinksResponse] = await Promise.all([ + await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/traces/{traceId}/span_links/{spanId}/children', + params: { + path: { traceId, spanId }, + query: { + kuery, + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + }, + }, + }), + apmApiClient.readUser({ + endpoint: 'GET /internal/apm/traces/{traceId}/span_links/{spanId}/parents', + params: { + path: { traceId, spanId }, + query: { + kuery, + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + processorEvent, + }, + }, + }), + ]); + + return { + childrenLinks: childrenLinksResponse.body, + parentsLinks: parentsLinksResponse.body, + }; + } + + describe('producer-internal-only span links details', () => { + let transactionALinksDetails: Awaited>; + let spanALinksDetails: Awaited>; + before(async () => { + const [transactionALinksDetailsResponse, spanALinksDetailsResponse] = await Promise.all( + [ + fetchChildrenAndParentsDetails({ + kuery: '', + traceId: ids.producerInternalOnly.traceId, + spanId: ids.producerInternalOnly.transactionAId, + processorEvent: ProcessorEvent.transaction, + }), + fetchChildrenAndParentsDetails({ + kuery: '', + traceId: ids.producerInternalOnly.traceId, + spanId: ids.producerInternalOnly.spanAId, + processorEvent: ProcessorEvent.span, + }), + ] + ); + transactionALinksDetails = transactionALinksDetailsResponse; + spanALinksDetails = spanALinksDetailsResponse; + }); + + it('returns no links for transaction A', () => { + expect(transactionALinksDetails.childrenLinks.spanLinksDetails).to.eql([]); + expect(transactionALinksDetails.parentsLinks.spanLinksDetails).to.eql([]); + }); + + it('returns no parents on Span A', () => { + expect(spanALinksDetails.parentsLinks.spanLinksDetails).to.eql([]); + }); + + it('returns two children on Span A', () => { + expect(spanALinksDetails.childrenLinks.spanLinksDetails.length).to.eql(2); + const serviceCDetails = spanALinksDetails.childrenLinks.spanLinksDetails.find( + (childDetails) => { + return ( + childDetails.traceId === ids.producerConsumer.traceId && + childDetails.spanId === ids.producerConsumer.transactionCId + ); + } + ); + expect(serviceCDetails?.details).to.eql({ + serviceName: 'producer-consumer', + agentName: 'ruby', + transactionId: ids.producerConsumer.transactionCId, + spanName: 'Transaction C', + duration: 1000000, + }); + + const serviceDDetails = spanALinksDetails.childrenLinks.spanLinksDetails.find( + (childDetails) => { + return ( + childDetails.traceId === ids.producerMultiple.traceId && + childDetails.spanId === ids.producerMultiple.transactionDId + ); + } + ); + expect(serviceDDetails?.details).to.eql({ + serviceName: 'consumer-multiple', + agentName: 'nodejs', + transactionId: ids.producerMultiple.transactionDId, + spanName: 'Transaction D', + duration: 1000000, + }); + }); + }); + + describe('producer-external-only span links details', () => { + let transactionBLinksDetails: Awaited>; + let spanBLinksDetails: Awaited>; + before(async () => { + const [transactionALinksDetailsResponse, spanALinksDetailsResponse] = await Promise.all( + [ + fetchChildrenAndParentsDetails({ + kuery: '', + traceId: ids.producerExternalOnly.traceId, + spanId: ids.producerExternalOnly.transactionBId, + processorEvent: ProcessorEvent.transaction, + }), + fetchChildrenAndParentsDetails({ + kuery: '', + traceId: ids.producerExternalOnly.traceId, + spanId: ids.producerExternalOnly.spanBId, + processorEvent: ProcessorEvent.span, + }), + ] + ); + transactionBLinksDetails = transactionALinksDetailsResponse; + spanBLinksDetails = spanALinksDetailsResponse; + }); + + it('returns producer-consumer as children of transaction B', () => { + expect(transactionBLinksDetails.childrenLinks.spanLinksDetails.length).to.be(1); + }); + + it('returns no parent for transaction B', () => { + expect(transactionBLinksDetails.parentsLinks.spanLinksDetails).to.eql([]); + }); + + it('returns external parent on Span B', () => { + expect(spanBLinksDetails.parentsLinks.spanLinksDetails.length).to.be(1); + expect(spanBLinksDetails.parentsLinks.spanLinksDetails).to.eql([ + { traceId: 'trace#1', spanId: 'span#1' }, + ]); + }); + + it('returns consumer-multiple as child on Span B', () => { + expect(spanBLinksDetails.childrenLinks.spanLinksDetails.length).to.be(1); + expect(spanBLinksDetails.childrenLinks.spanLinksDetails).to.eql([ + { + traceId: ids.producerMultiple.traceId, + spanId: ids.producerMultiple.spanEId, + details: { + serviceName: 'consumer-multiple', + agentName: 'nodejs', + transactionId: ids.producerMultiple.transactionDId, + spanName: 'Span E', + duration: 100000, + spanSubtype: 'http', + spanType: 'external', + }, + }, + ]); + }); + }); + + describe('producer-consumer span links details', () => { + let transactionCLinksDetails: Awaited>; + let spanCLinksDetails: Awaited>; + before(async () => { + const [transactionALinksDetailsResponse, spanALinksDetailsResponse] = await Promise.all( + [ + fetchChildrenAndParentsDetails({ + kuery: '', + traceId: ids.producerConsumer.traceId, + spanId: ids.producerConsumer.transactionCId, + processorEvent: ProcessorEvent.transaction, + }), + fetchChildrenAndParentsDetails({ + kuery: '', + traceId: ids.producerConsumer.traceId, + spanId: ids.producerConsumer.spanCId, + processorEvent: ProcessorEvent.span, + }), + ] + ); + transactionCLinksDetails = transactionALinksDetailsResponse; + spanCLinksDetails = spanALinksDetailsResponse; + }); + + it('returns producer-internal-only Span A, producer-external-only Transaction B, and External link as parents of Transaction C', () => { + expect(transactionCLinksDetails.parentsLinks.spanLinksDetails.length).to.be(3); + expect(transactionCLinksDetails.parentsLinks.spanLinksDetails).to.eql([ + { + traceId: ids.producerInternalOnly.traceId, + spanId: ids.producerInternalOnly.spanAId, + details: { + serviceName: 'producer-internal-only', + agentName: 'go', + transactionId: ids.producerInternalOnly.transactionAId, + spanName: 'Span A', + duration: 100000, + spanSubtype: 'http', + spanType: 'external', + }, + }, + { + traceId: ids.producerExternalOnly.traceId, + spanId: ids.producerExternalOnly.transactionBId, + details: { + serviceName: 'producer-external-only', + agentName: 'java', + transactionId: ids.producerExternalOnly.transactionBId, + duration: 1000000, + spanName: 'Transaction B', + }, + }, + { + traceId: ids.producerConsumer.externalTraceId, + spanId: ids.producerExternalOnly.spanBId, + }, + ]); + }); + + it('returns consumer-multiple Span E as child of Transaction C', () => { + expect(transactionCLinksDetails.childrenLinks.spanLinksDetails.length).to.be(1); + expect(transactionCLinksDetails.childrenLinks.spanLinksDetails).to.eql([ + { + traceId: ids.producerMultiple.traceId, + spanId: ids.producerMultiple.spanEId, + details: { + serviceName: 'consumer-multiple', + agentName: 'nodejs', + transactionId: ids.producerMultiple.transactionDId, + spanName: 'Span E', + duration: 100000, + spanSubtype: 'http', + spanType: 'external', + }, + }, + ]); + }); + + it('returns no child on Span C', () => { + expect(spanCLinksDetails.parentsLinks.spanLinksDetails.length).to.be(0); + }); + + it('returns consumer-multiple as Child on producer-consumer', () => { + expect(spanCLinksDetails.childrenLinks.spanLinksDetails.length).to.be(1); + expect(spanCLinksDetails.childrenLinks.spanLinksDetails).to.eql([ + { + traceId: ids.producerMultiple.traceId, + spanId: ids.producerMultiple.transactionDId, + details: { + serviceName: 'consumer-multiple', + agentName: 'nodejs', + transactionId: ids.producerMultiple.transactionDId, + spanName: 'Transaction D', + duration: 1000000, + }, + }, + ]); + }); + }); + + describe('consumer-multiple span links details', () => { + let transactionDLinksDetails: Awaited>; + let spanELinksDetails: Awaited>; + before(async () => { + const [transactionALinksDetailsResponse, spanALinksDetailsResponse] = await Promise.all( + [ + fetchChildrenAndParentsDetails({ + kuery: '', + traceId: ids.producerMultiple.traceId, + spanId: ids.producerMultiple.transactionDId, + processorEvent: ProcessorEvent.transaction, + }), + fetchChildrenAndParentsDetails({ + kuery: '', + traceId: ids.producerMultiple.traceId, + spanId: ids.producerMultiple.spanEId, + processorEvent: ProcessorEvent.span, + }), + ] + ); + transactionDLinksDetails = transactionALinksDetailsResponse; + spanELinksDetails = spanALinksDetailsResponse; + }); + + it('returns producer-internal-only Span A and producer-consumer Span C as parents of Transaction D', () => { + expect(transactionDLinksDetails.parentsLinks.spanLinksDetails.length).to.be(2); + expect(transactionDLinksDetails.parentsLinks.spanLinksDetails).to.eql([ + { + traceId: ids.producerInternalOnly.traceId, + spanId: ids.producerInternalOnly.spanAId, + details: { + serviceName: 'producer-internal-only', + agentName: 'go', + transactionId: ids.producerInternalOnly.transactionAId, + spanName: 'Span A', + duration: 100000, + spanSubtype: 'http', + spanType: 'external', + }, + }, + { + traceId: ids.producerConsumer.traceId, + spanId: ids.producerConsumer.spanCId, + details: { + serviceName: 'producer-consumer', + agentName: 'ruby', + transactionId: ids.producerConsumer.transactionCId, + spanName: 'Span C', + duration: 100000, + spanSubtype: 'http', + spanType: 'external', + }, + }, + ]); + }); + + it('returns no children on Transaction D', () => { + expect(transactionDLinksDetails.childrenLinks.spanLinksDetails.length).to.be(0); + }); + + it('returns producer-external-only Span B and producer-consumer Transaction C as parents of Span E', () => { + expect(spanELinksDetails.parentsLinks.spanLinksDetails.length).to.be(2); + + expect(spanELinksDetails.parentsLinks.spanLinksDetails).to.eql([ + { + traceId: ids.producerExternalOnly.traceId, + spanId: ids.producerExternalOnly.spanBId, + details: { + serviceName: 'producer-external-only', + agentName: 'java', + transactionId: ids.producerExternalOnly.transactionBId, + spanName: 'Span B', + duration: 100000, + spanSubtype: 'http', + spanType: 'external', + }, + }, + { + traceId: ids.producerConsumer.traceId, + spanId: ids.producerConsumer.transactionCId, + details: { + serviceName: 'producer-consumer', + agentName: 'ruby', + transactionId: ids.producerConsumer.transactionCId, + spanName: 'Transaction C', + duration: 1000000, + }, + }, + ]); + }); + + it('returns no children on Span E', () => { + expect(spanELinksDetails.childrenLinks.spanLinksDetails.length).to.be(0); + }); + }); + }); + } + ); +} diff --git a/x-pack/test/apm_api_integration/tests/traces/trace_by_id.spec.ts b/x-pack/test/apm_api_integration/tests/traces/trace_by_id.spec.ts index 97fcb49d854dc..43f52bed594b2 100644 --- a/x-pack/test/apm_api_integration/tests/traces/trace_by_id.spec.ts +++ b/x-pack/test/apm_api_integration/tests/traces/trace_by_id.spec.ts @@ -4,84 +4,120 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { apm, EntityArrayIterable, timerange } from '@elastic/apm-synthtrace'; import expect from '@kbn/expect'; -import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { SupertestReturnType } from '../../common/apm_api_supertest'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); + const synthtraceEsClient = getService('synthtraceEsClient'); + + const start = new Date('2022-01-01T00:00:00.000Z').getTime(); + const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; - const archiveName = 'apm_8.0.0'; - const metadata = archives_metadata[archiveName]; - const { start, end } = metadata; + async function fetchTraces({ + traceId, + query, + }: { + traceId: string; + query: { start: string; end: string; _inspect?: boolean }; + }) { + return await apmApiClient.readUser({ + endpoint: `GET /internal/apm/traces/{traceId}`, + params: { + path: { traceId }, + query, + }, + }); + } registry.when('Trace does not exist', { config: 'basic', archives: [] }, () => { it('handles empty state', async () => { - const response = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/traces/{traceId}`, - params: { - path: { traceId: 'foo' }, - query: { start, end }, + const response = await fetchTraces({ + traceId: 'foo', + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), }, }); expect(response.status).to.be(200); - expect(response.body).to.eql({ exceedsMax: false, traceDocs: [], errorDocs: [] }); + expect(response.body).to.eql({ + exceedsMax: false, + traceDocs: [], + errorDocs: [], + linkedChildrenOfSpanCountBySpanId: {}, + }); }); }); - registry.when('Trace exists', { config: 'basic', archives: [archiveName] }, () => { - let response: SupertestReturnType<`GET /internal/apm/traces/{traceId}`>; + registry.when('Trace exists', { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, () => { + let serviceATraceId: string; before(async () => { - response = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/traces/{traceId}`, - params: { - path: { traceId: '64d0014f7530df24e549dd17cc0a8895' }, - query: { start, end }, - }, - }); - }); + const instanceJava = apm.service('synth-apple', 'production', 'java').instance('instance-b'); + const events = timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return [ + instanceJava + .transaction('GET /apple ๐Ÿ') + .timestamp(timestamp) + .duration(1000) + .failure() + .errors( + instanceJava + .error('[ResponseError] index_not_found_exception') + .timestamp(timestamp + 50) + ) + .children( + instanceJava + .span('get_green_apple_๐Ÿ', 'db', 'elasticsearch') + .timestamp(timestamp + 50) + .duration(900) + .success() + ), + ]; + }); + const entities = events.toArray(); + serviceATraceId = entities.slice(0, 1)[0]['trace.id']!; - it('returns the correct status code', async () => { - expect(response.status).to.be(200); + await synthtraceEsClient.index(new EntityArrayIterable(entities)); }); - it('returns the correct number of buckets', async () => { - expectSnapshot(response.body.errorDocs.map((doc) => doc.error?.exception?.[0]?.message)) - .toMatchInline(` - Array [ - "Test CaptureError", - "Uncaught Error: Test Error in dashboard", - ] - `); - expectSnapshot( - response.body.traceDocs.map((doc) => - 'span' in doc ? `${doc.span.name} (span)` : `${doc.transaction.name} (transaction)` - ) - ).toMatchInline(` - Array [ - "/dashboard (transaction)", - "GET /api/stats (transaction)", - "APIRestController#topProducts (transaction)", - "Parsing the document, executing sync. scripts (span)", - "GET /api/products/top (span)", - "GET /api/stats (span)", - "Requesting and receiving the document (span)", - "SELECT FROM customers (span)", - "SELECT FROM order_lines (span)", - "http://opbeans-frontend:3000/static/css/main.7bd7c5e8.css (span)", - "SELECT FROM products (span)", - "SELECT FROM orders (span)", - "SELECT FROM order_lines (span)", - "Making a connection to the server (span)", - "Fire \\"load\\" event (span)", - "empty query (span)", - ] - `); - expectSnapshot(response.body.exceedsMax).toMatchInline(`false`); + after(() => synthtraceEsClient.clean()); + + describe('return trace', () => { + let traces: Awaited>['body']; + before(async () => { + const response = await fetchTraces({ + traceId: serviceATraceId, + query: { start: new Date(start).toISOString(), end: new Date(end).toISOString() }, + }); + expect(response.status).to.eql(200); + traces = response.body; + }); + it('returns some errors', () => { + expect(traces.errorDocs.length).to.be.greaterThan(0); + expect(traces.errorDocs[0].error.exception?.[0].message).to.eql( + '[ResponseError] index_not_found_exception' + ); + }); + + it('returns some trace docs', () => { + expect(traces.traceDocs.length).to.be.greaterThan(0); + expect( + traces.traceDocs.map((item) => { + if (item.span && 'name' in item.span) { + return item.span.name; + } + if (item.transaction && 'name' in item.transaction) { + return item.transaction.name; + } + }) + ).to.eql(['GET /apple ๐Ÿ', 'get_green_apple_๐Ÿ']); + }); }); }); } diff --git a/yarn.lock b/yarn.lock index 19882f1516155..ef13d76303550 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1396,29 +1396,29 @@ dependencies: tslib "^2.0.0" -"@elastic/apm-rum-core@^5.15.0": - version "5.15.0" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-5.15.0.tgz#f1067078080be1b7167c72ae6d155b0ed5cdf6c6" - integrity sha512-T5/1hZPskmU6N3Xo2CRNi5tX2ht8R5nLmh5t0I1v8RxkwbQms47AR1f0ZVvXN7W2FCDPadyQXC3f9do3k5A6OA== +"@elastic/apm-rum-core@^5.16.0": + version "5.16.0" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-5.16.0.tgz#c3f6aaaee005717f10578877a3d1c7e6894eb63f" + integrity sha512-5aCwlKdmitM5Jk8wR7WcCtJzejIlSaUUHOGWANvq79GDtcCIjE/yD44pft8UAYQpiI28WXCLAFvJvIQiUzl/nw== dependencies: error-stack-parser "^1.3.5" opentracing "^0.14.3" promise-polyfill "^8.1.3" -"@elastic/apm-rum-react@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-1.4.0.tgz#f36453e54d2ebdedb0d6d0c4f6cc76e16304b118" - integrity sha512-YIBuEJN6fkiB1M/o84PF4lQheAjrd3PQCm6t8pP4dKuWN1cWZnSsojnuGacx2bJn1kWWZxVDQ7wTjPJutkIy2A== +"@elastic/apm-rum-react@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-1.4.1.tgz#d46913f1c3aa7f5e54d1898b644ffd7e0b75129a" + integrity sha512-bRyqVxe9QY40imv5u0p7q4WaXUDMs2gHewPuADC2LGiX8piNfpRXA7jj3KPD4P/045dlDsmvVjV0AELLyNipuQ== dependencies: - "@elastic/apm-rum" "^5.11.0" + "@elastic/apm-rum" "^5.11.1" hoist-non-react-statics "^3.3.0" -"@elastic/apm-rum@^5.11.0": - version "5.11.0" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-5.11.0.tgz#97dbc426d0ec27b46e78a649f73d9e4a198ae258" - integrity sha512-+98NLG4NDa7o1DCtkhXeGmKW5riDPHSpgy2UxzLK4j02ZPBOccOUjIw5F8yZAUsrPUpQmk39x13IJl0mFyzjyA== +"@elastic/apm-rum@^5.11.1": + version "5.11.1" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-5.11.1.tgz#ecbef3935ded2e9da338e6a749ab03b26e40c222" + integrity sha512-d6IxNvCxufb6JvVEH6TI7stwabJSIIBJsZVnRNaromZAgVOc5woI8mB35AH5glEPUb95KuL9CkCObHlqGpAt5w== dependencies: - "@elastic/apm-rum-core" "^5.15.0" + "@elastic/apm-rum-core" "^5.16.0" "@elastic/apm-synthtrace@link:bazel-bin/packages/elastic-apm-synthtrace": version "0.0.0" @@ -1499,10 +1499,6 @@ semver "^7.3.2" topojson-client "^3.1.0" -"@elastic/eslint-config-kibana@link:bazel-bin/packages/elastic-eslint-config-kibana": - version "0.0.0" - uid "" - "@elastic/eslint-plugin-eui@0.0.2": version "0.0.2" resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.0.2.tgz#56b9ef03984a05cc213772ae3713ea8ef47b0314" @@ -3017,6 +3013,10 @@ version "0.0.0" uid "" +"@kbn/eslint-config@link:bazel-bin/packages/kbn-eslint-config": + version "0.0.0" + uid "" + "@kbn/eslint-plugin-eslint@link:bazel-bin/packages/kbn-eslint-plugin-eslint": version "0.0.0" uid ""