diff --git a/.eslintrc.js b/.eslintrc.js index a04a745fcf0e7..6bf88444aaf70 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1997,9 +1997,6 @@ module.exports = { }, { files: [ - // logsShared depends on o11y/private plugins, but platform plugins depend on it - 'x-pack/plugins/observability_solution/logs_shared/**', - // TODO @kibana/operations 'scripts/create_observability_rules.js', // is importing "@kbn/observability-alerting-test-data" (observability/private) 'src/cli_setup/**', // is importing "@kbn/interactive-setup-plugin" (platform/private) diff --git a/packages/core/application/core-application-common/index.ts b/packages/core/application/core-application-common/index.ts index 633085dc95c1c..25d375dc7c985 100644 --- a/packages/core/application/core-application-common/index.ts +++ b/packages/core/application/core-application-common/index.ts @@ -10,3 +10,4 @@ export type { AppCategory } from './src/app_category'; export { APP_WRAPPER_CLASS } from './src/app_wrapper_class'; export { DEFAULT_APP_CATEGORIES } from './src/default_app_categories'; +export { GlobalAppStyle } from './src/global_app_style'; diff --git a/packages/core/application/core-application-common/src/global_app_style.tsx b/packages/core/application/core-application-common/src/global_app_style.tsx new file mode 100644 index 0000000000000..595602385da86 --- /dev/null +++ b/packages/core/application/core-application-common/src/global_app_style.tsx @@ -0,0 +1,164 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { css, Global } from '@emotion/react'; +import { useEuiTheme, type UseEuiTheme } from '@elastic/eui'; + +export const renderingOverrides = (euiTheme: UseEuiTheme['euiTheme']) => css` + #kibana-body { + // DO NOT ADD ANY OVERFLOW BEHAVIORS HERE + // It will break the sticky navigation + min-height: 100%; + display: flex; + flex-direction: column; + } + + // Affixes a div to restrict the position of charts tooltip to the visible viewport minus the header + #app-fixed-viewport { + pointer-events: none; + visibility: hidden; + position: fixed; + top: var(--kbnAppHeadersOffset, var(--euiFixedHeadersOffset, 0)); + right: 0; + bottom: 0; + left: 0; + } + + .kbnAppWrapper { + // DO NOT ADD ANY OTHER STYLES TO THIS SELECTOR + // This a very nested dependency happening in "all" apps + display: flex; + flex-flow: column nowrap; + flex-grow: 1; + z-index: 0; // This effectively puts every high z-index inside the scope of this wrapper to it doesn't interfere with the header and/or overlay mask + position: relative; // This is temporary for apps that relied on this being present on \`.application\` + } + + .kbnBody { + padding-top: var(--euiFixedHeadersOffset, 0); + } + + // Conditionally override :root CSS fixed header variable. Updating \`--euiFixedHeadersOffset\` + //on the body will cause all child EUI components to automatically update their offsets + .kbnBody--hasHeaderBanner { + --euiFixedHeadersOffset: var(--kbnHeaderOffsetWithBanner); + + // Offset fixed EuiHeaders by the top banner + .euiHeader[data-fixed-header] { + margin-top: var(--kbnHeaderBannerHeight); + } + + // Prevent banners from covering full screen data grids + .euiDataGrid--fullScreen { + height: calc(100vh - var(--kbnHeaderBannerHeight)); + top: var(--kbnHeaderBannerHeight); + } + } + + // Set a body CSS variable for the app container to use - calculates the total + // height of all fixed headers + the sticky action menu toolbar + .kbnBody--hasProjectActionMenu { + --kbnAppHeadersOffset: calc( + var(--kbnHeaderOffset) + var(--kbnProjectHeaderAppActionMenuHeight) + ); + + &.kbnBody--hasHeaderBanner { + --kbnAppHeadersOffset: calc( + var(--kbnHeaderOffsetWithBanner) + var(--kbnProjectHeaderAppActionMenuHeight) + ); + } + } + + .kbnBody--chromeHidden { + // stylelint-disable-next-line length-zero-no-unit + --euiFixedHeadersOffset: 0px; + + &.kbnBody--hasHeaderBanner { + --euiFixedHeadersOffset: var(--kbnHeaderBannerHeight); + } + + &.kbnBody--hasProjectActionMenu { + --kbnAppHeadersOffset: var(--euiFixedHeadersOffset, 0); + } + } +`; + +export const bannerStyles = (euiTheme: UseEuiTheme['euiTheme']) => css` + .header__topBanner { + position: fixed; + top: 0; + left: 0; + height: var(--kbnHeaderBannerHeight); + width: 100%; + z-index: ${euiTheme.levels.header}; + } + + .header__topBannerContainer { + height: 100%; + width: 100%; + } +`; + +export const chromeStyles = (euiTheme: UseEuiTheme['euiTheme']) => css` + .euiDataGrid__restrictBody { + .headerGlobalNav, + .kbnQueryBar { + display: none; + } + } + + .euiDataGrid__restrictBody.euiBody--headerIsFixed { + .euiFlyout { + top: 0; + height: 100%; + } + } + + .chrHeaderHelpMenu__version { + text-transform: none; + } + + .chrHeaderBadge__wrapper { + align-self: center; + margin-right: ${euiTheme.size.base}; + } + + .header__toggleNavButtonSection { + .euiBody--collapsibleNavIsDocked & { + display: none; + } + } + + .header__breadcrumbsWithExtensionContainer { + overflow: hidden; // enables text-ellipsis in the last breadcrumb + .euiHeaderBreadcrumbs { + // stop breadcrumbs from growing. + // this makes the extension appear right next to the last breadcrumb + flex-grow: 0; + margin-right: 0; + + overflow: hidden; // enables text-ellipsis in the last breadcrumb + } + } + .header__breadcrumbsAppendExtension { + flex-grow: 1; + } +`; + +export const GlobalAppStyle = () => { + const { euiTheme } = useEuiTheme(); + return ( + + ); +}; diff --git a/packages/core/rendering/core-rendering-browser-internal/src/rendering_service.tsx b/packages/core/rendering/core-rendering-browser-internal/src/rendering_service.tsx index 1995d6c013cf6..9d5982bd40d35 100644 --- a/packages/core/rendering/core-rendering-browser-internal/src/rendering_service.tsx +++ b/packages/core/rendering/core-rendering-browser-internal/src/rendering_service.tsx @@ -20,6 +20,7 @@ import type { ThemeServiceStart } from '@kbn/core-theme-browser'; import type { UserProfileService } from '@kbn/core-user-profile-browser'; import { KibanaRootContextProvider } from '@kbn/react-kibana-context-root'; import { APP_FIXED_VIEWPORT_ID } from '@kbn/core-rendering-browser'; +import { GlobalAppStyle } from '@kbn/core-application-common'; import { AppWrapper } from './app_containers'; interface StartServices { @@ -62,6 +63,9 @@ export class RenderingService { ReactDOM.render( <> + {/* Global Styles that apply across the entire app */} + + {/* Fixed headers */} {chromeHeader} diff --git a/packages/kbn-relocate/relocate.ts b/packages/kbn-relocate/relocate.ts index fe2537ddeb040..5852d8fb5be04 100644 --- a/packages/kbn-relocate/relocate.ts +++ b/packages/kbn-relocate/relocate.ts @@ -15,7 +15,7 @@ import { orderBy } from 'lodash'; import type { ToolingLog } from '@kbn/tooling-log'; import { getPackages } from '@kbn/repo-packages'; import { REPO_ROOT } from '@kbn/repo-info'; -import type { Package } from './types'; +import type { Package, PullRequest } from './types'; import { DESCRIPTION, EXCLUDED_MODULES, KIBANA_FOLDER, NEW_BRANCH } from './constants'; import { belongsTo, @@ -26,7 +26,15 @@ import { } from './utils/relocate'; import { safeExec } from './utils/exec'; import { relocatePlan, relocateSummary } from './utils/logging'; -import { checkoutBranch, checkoutResetPr, findGithubLogin, findRemoteName } from './utils/git'; +import { + checkoutBranch, + checkoutResetPr, + cherryPickManualCommits, + findGithubLogin, + findPr, + findRemoteName, + getManualCommits, +} from './utils/git'; const moveModule = async (module: Package, log: ToolingLog) => { const destination = calculateModuleTargetFolder(module); @@ -128,6 +136,9 @@ export const findAndMoveModule = async (moduleId: string, log: ToolingLog) => { }; export const findAndRelocateModules = async (params: RelocateModulesParams, log: ToolingLog) => { + const { prNumber, baseBranch, ...findParams } = params; + let pr: PullRequest | undefined; + const upstream = await findRemoteName('elastic/kibana'); if (!upstream) { log.error( @@ -142,8 +153,6 @@ export const findAndRelocateModules = async (params: RelocateModulesParams, log: return; } - const { prNumber, baseBranch, ...findParams } = params; - const toMove = findModules(findParams, log); if (!toMove.length) { log.info( @@ -153,40 +162,60 @@ export const findAndRelocateModules = async (params: RelocateModulesParams, log: } relocatePlan(toMove, log); - const res1 = await inquirer.prompt({ + + const resConfirmPlan = await inquirer.prompt({ type: 'confirm', name: 'confirmPlan', message: `The script will RESET CHANGES in this repository, relocate the modules above and update references. Proceed?`, }); - if (!res1.confirmPlan) { + if (!resConfirmPlan.confirmPlan) { log.info('Aborting'); return; } + if (prNumber) { + pr = await findPr(prNumber); + + if (getManualCommits(pr.commits).length > 0) { + const resOverride = await inquirer.prompt({ + type: 'confirm', + name: 'overrideManualCommits', + message: 'Detected manual commits in the PR, do you want to override them?', + }); + if (!resOverride.overrideManualCommits) { + return; + } + } + } + // start with a clean repo await safeExec(`git restore --staged .`); await safeExec(`git restore .`); await safeExec(`git clean -f -d`); await safeExec(`git checkout ${baseBranch} && git pull ${upstream} ${baseBranch}`); - if (prNumber) { + if (pr) { // checkout existing PR, reset all commits, rebase from baseBranch try { - if (!(await checkoutResetPr(baseBranch, prNumber))) { - log.info('Aborting'); - return; - } + await checkoutResetPr(pr, baseBranch); } catch (error) { log.error(`Error checking out / resetting PR #${prNumber}:`); log.error(error); return; } } else { - // checkout [new] branch + // checkout new branch await checkoutBranch(NEW_BRANCH); } + // push changes in the branch + await inquirer.prompt({ + type: 'confirm', + name: 'readyRelocate', + message: `Ready to relocate! You can commit changes previous to the relocation at this point. Confirm to proceed with the relocation`, + }); + // relocate modules await safeExec(`yarn kbn bootstrap`); const movedCount = await relocateModules(toMove, log); @@ -197,10 +226,15 @@ export const findAndRelocateModules = async (params: RelocateModulesParams, log: ); return; } + relocateSummary(log); + if (pr) { + await cherryPickManualCommits(pr, log); + } + // push changes in the branch - const res2 = await inquirer.prompt({ + const resPushBranch = await inquirer.prompt({ type: 'confirm', name: 'pushBranch', message: `Relocation finished! You can commit extra changes at this point. Confirm to proceed pushing the current branch`, @@ -210,7 +244,7 @@ export const findAndRelocateModules = async (params: RelocateModulesParams, log: ? `git push --force-with-lease` : `git push --set-upstream ${origin} ${NEW_BRANCH}`; - if (!res2.pushBranch) { + if (!resPushBranch.pushBranch) { log.info(`Remember to push changes with "${pushCmd}"`); return; } diff --git a/packages/kbn-relocate/types.ts b/packages/kbn-relocate/types.ts index 391cef336d639..2f030bb68ae7e 100644 --- a/packages/kbn-relocate/types.ts +++ b/packages/kbn-relocate/types.ts @@ -14,6 +14,7 @@ export interface CommitAuthor { } export interface Commit { + oid: string; messageHeadline: string; authors: CommitAuthor[]; } diff --git a/packages/kbn-relocate/utils/git.ts b/packages/kbn-relocate/utils/git.ts index f2e529bee6d0f..0085e07fdd6b5 100644 --- a/packages/kbn-relocate/utils/git.ts +++ b/packages/kbn-relocate/utils/git.ts @@ -8,17 +8,24 @@ */ import inquirer from 'inquirer'; +import type { ToolingLog } from '@kbn/tooling-log'; import type { Commit, PullRequest } from '../types'; import { safeExec } from './exec'; export const findRemoteName = async (repo: string) => { - const res = await safeExec('git remote -v'); - const remotes = res.stdout.split('\n').map((line) => line.split(/\t| /).filter(Boolean)); - return remotes.find(([_, url]) => url.includes(`github.com/${repo}`))?.[0]; + const res = await safeExec('git remote -v', true, false); + const remotes = res.stdout + .trim() + .split('\n') + .map((line) => line.split(/\t| /).filter(Boolean)) + .filter((chunks) => chunks.length >= 2); + return remotes.find( + ([, url]) => url.includes(`github.com/${repo}`) || url.includes(`github.com:${repo}`) + )?.[0]; }; export const findGithubLogin = async () => { - const res = await safeExec('gh auth status'); + const res = await safeExec('gh auth status', true, false); // e.g. ✓ Logged in to github.com account gsoldevila (/Users/gsoldevila/.config/gh/hosts.yml) const loginLine = res.stdout .split('\n') @@ -34,17 +41,16 @@ export const findPr = async (number: string): Promise => { return { ...JSON.parse(res.stdout), number }; }; -export function hasManualCommits(commits: Commit[]) { - const manualCommits = commits.filter( - (commit) => - !commit.messageHeadline.startsWith('Relocating module ') && - !commit.messageHeadline.startsWith('Moving modules owned by ') && - commit.authors.some( - (author) => author.login !== 'kibanamachine' && author.login !== 'elasticmachine' - ) +export const isManualCommit = (commit: Commit) => + !commit.messageHeadline.startsWith('Relocating module ') && + !commit.messageHeadline.startsWith('Moving modules owned by ') && + !commit.messageHeadline.startsWith('Merge branch ') && + commit.authors.some( + (author) => author.login !== 'kibanamachine' && author.login !== 'elasticmachine' ); - return manualCommits.length > 0; +export function getManualCommits(commits: Commit[]) { + return commits.filter(isManualCommit); } export async function getLastCommitMessage() { @@ -87,33 +93,14 @@ async function deleteBranches(...branchNames: string[]) { ); } -export const checkoutResetPr = async (baseBranch: string, prNumber: string): Promise => { - const pr = await findPr(prNumber); - - if (hasManualCommits(pr.commits)) { - const res = await inquirer.prompt({ - type: 'confirm', - name: 'overrideManualCommits', - message: 'Detected manual commits in the PR, do you want to override them?', - }); - if (!res.overrideManualCommits) { - return false; - } - } - - // previous cleanup on current branch - await safeExec(`git restore --staged .`); - await safeExec(`git restore .`); - await safeExec(`git clean -f -d`); - +export const checkoutResetPr = async (pr: PullRequest, baseBranch: string) => { // delete existing branch await deleteBranches(pr.headRefName); // checkout the PR branch - await safeExec(`gh pr checkout ${prNumber}`); + await safeExec(`gh pr checkout ${pr.number}`); await resetAllCommits(pr.commits.length); await safeExec(`git rebase ${baseBranch}`); - return true; }; export const checkoutBranch = async (branch: string) => { @@ -124,3 +111,71 @@ export const checkoutBranch = async (branch: string) => { await safeExec(`git checkout -b ${branch}`); } }; + +export const cherryPickManualCommits = async (pr: PullRequest, log: ToolingLog) => { + const manualCommits = getManualCommits(pr.commits); + if (manualCommits.length) { + log.info(`Found manual commits on https://github.com/elastic/kibana/pull/${pr.number}/commits`); + + for (let i = 0; i < manualCommits.length; ++i) { + const { oid, messageHeadline, authors } = manualCommits[i]; + const url = `https://github.com/elastic/kibana/pull/${pr.number}/commits/${oid}`; + + const res = await inquirer.prompt({ + type: 'list', + choices: [ + { name: 'Yes, attempt to cherry-pick', value: 'yes' }, + { name: 'No, I will add it manually (press when finished)', value: 'no' }, + ], + name: 'cherryPick', + message: `Do you want to cherry pick '${messageHeadline}' (${authors[0].login})?`, + }); + + if (res.cherryPick === 'yes') { + try { + await safeExec(`git cherry-pick ${oid}`); + log.info(`Commit '${messageHeadline}' (${authors[0].login}) cherry-picked successfully!`); + } catch (error) { + log.info(`Error trying to cherry-pick: ${url}`); + log.error(error.message); + const res2 = await inquirer.prompt({ + type: 'list', + choices: [ + { name: 'Abort this cherry-pick', value: 'abort' }, + { name: 'Conflicts solved (git cherry-pick --continue)', value: 'continue' }, + { name: 'I solved the conflicts and commited', value: 'done' }, + ], + name: 'cherryPickFailed', + message: `Automatic cherry-pick failed, manual intervention required`, + }); + + if (res2.cherryPickFailed === 'abort') { + try { + await safeExec(`git cherry-pick --abort`); + log.warning( + 'Cherry-pick aborted, please review changes in that commit and apply them manually if needed!' + ); + } catch (error2) { + log.error( + 'Cherry-pick --abort failed, please cleanup your working tree before continuing!' + ); + } + } else if (res2.cherryPickFailed === 'continue') { + try { + await safeExec(`git cherry-pick --continue`); + log.info( + `Commit '${messageHeadline}' (${authors[0].login}) cherry-picked successfully!` + ); + } catch (error2) { + await inquirer.prompt({ + type: 'confirm', + name: 'cherryPickContinueFailed', + message: `Cherry pick --continue failed, please address conflicts AND COMMIT manually. Hit confirm when ready`, + }); + } + } + } + } + } + } +}; diff --git a/packages/kbn-relocate/utils/logging.ts b/packages/kbn-relocate/utils/logging.ts index 742610dfe1de6..4aec07a1d9bf9 100644 --- a/packages/kbn-relocate/utils/logging.ts +++ b/packages/kbn-relocate/utils/logging.ts @@ -10,6 +10,7 @@ import type { ToolingLog } from '@kbn/tooling-log'; import { appendFileSync, writeFileSync } from 'fs'; import dedent from 'dedent'; +import Table from 'cli-table3'; import type { Package } from '../types'; import { calculateModuleTargetFolder } from './relocate'; import { @@ -21,6 +22,20 @@ import { UPDATED_RELATIVE_PATHS, } from '../constants'; +export const createModuleTable = (entries: string[][]) => { + const table = new Table({ + head: ['Id', 'Target folder'], + colAligns: ['left', 'left'], + style: { + 'padding-left': 2, + 'padding-right': 2, + }, + }); + + table.push(...entries); + return table; +}; + export const relocatePlan = (modules: Package[], log: ToolingLog) => { const plugins = modules.filter((module) => module.manifest.type === 'plugin'); const packages = modules.filter((module) => module.manifest.type !== 'plugin'); @@ -37,11 +52,8 @@ export const relocatePlan = (modules: Package[], log: ToolingLog) => { \n\n`; appendFileSync(DESCRIPTION, pluginList); - log.info( - `${plugins.length} plugin(s) are going to be relocated:\n${plugins - .map((plg) => `${plg.id} => ${target(plg)}`) - .join('\n')}` - ); + const plgTable = createModuleTable(plugins.map((plg) => [plg.id, target(plg)])); + log.info(`${plugins.length} plugin(s) are going to be relocated:\n${plgTable.toString()}`); } if (packages.length) { @@ -53,11 +65,8 @@ export const relocatePlan = (modules: Package[], log: ToolingLog) => { \n\n`; appendFileSync(DESCRIPTION, packageList); - log.info( - `${packages.length} packages(s) are going to be relocated:\n${packages - .map((plg) => `${plg.id} => ${target(plg)}`) - .join('\n')}` - ); + const pkgTable = createModuleTable(packages.map((pkg) => [pkg.id, target(pkg)])); + log.info(`${packages.length} packages(s) are going to be relocated:\n${pkgTable.toString()}`); } }; diff --git a/packages/kbn-xstate-utils/kibana.jsonc b/packages/kbn-xstate-utils/kibana.jsonc index 3b1bcf6bf8d76..5638550a862df 100644 --- a/packages/kbn-xstate-utils/kibana.jsonc +++ b/packages/kbn-xstate-utils/kibana.jsonc @@ -4,6 +4,6 @@ "owner": [ "@elastic/obs-ux-logs-team" ], - "group": "observability", - "visibility": "private" + "group": "platform", + "visibility": "shared" } diff --git a/scripts/stage_by_owner.js b/scripts/stage_by_owner.js new file mode 100644 index 0000000000000..0ac33b3901a04 --- /dev/null +++ b/scripts/stage_by_owner.js @@ -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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +require('../src/setup_node_env'); +require('../src/dev/stage_by_owner'); diff --git a/src/core/public/styles/_index.scss b/src/core/public/styles/_index.scss index cfdb1c7192dcd..c8567498b42ec 100644 --- a/src/core/public/styles/_index.scss +++ b/src/core/public/styles/_index.scss @@ -1,3 +1 @@ @import './base'; -@import './chrome/index'; -@import './rendering/index'; diff --git a/src/core/public/styles/chrome/_banner.scss b/src/core/public/styles/chrome/_banner.scss deleted file mode 100644 index feb69e54a911f..0000000000000 --- a/src/core/public/styles/chrome/_banner.scss +++ /dev/null @@ -1,13 +0,0 @@ -.header__topBanner { - position: fixed; - top: 0; - left: 0; - height: var(--kbnHeaderBannerHeight); - width: 100%; - z-index: $euiZHeader; -} - -.header__topBannerContainer { - height: 100%; - width: 100%; -} diff --git a/src/core/public/styles/chrome/_index.scss b/src/core/public/styles/chrome/_index.scss deleted file mode 100644 index 22390aa5a44b3..0000000000000 --- a/src/core/public/styles/chrome/_index.scss +++ /dev/null @@ -1,45 +0,0 @@ -@import './banner'; - -.euiDataGrid__restrictBody { - .headerGlobalNav, - .kbnQueryBar { - display: none; - } -} - -.euiDataGrid__restrictBody.euiBody--headerIsFixed { - .euiFlyout { - top: 0; - height: 100%; - } -} - -.chrHeaderHelpMenu__version { - text-transform: none; -} - -.chrHeaderBadge__wrapper { - align-self: center; - margin-right: $euiSize; -} - -.header__toggleNavButtonSection { - .euiBody--collapsibleNavIsDocked & { - display: none; - } -} - -.header__breadcrumbsWithExtensionContainer { - overflow: hidden; // enables text-ellipsis in the last breadcrumb - .euiHeaderBreadcrumbs { - // stop breadcrumbs from growing. - // this makes the extension appear right next to the last breadcrumb - flex-grow: 0; - margin-right: 0; - - overflow: hidden; // enables text-ellipsis in the last breadcrumb - } -} -.header__breadcrumbsAppendExtension { - flex-grow: 1; -} diff --git a/src/core/public/styles/rendering/_base.scss b/src/core/public/styles/rendering/_base.scss deleted file mode 100644 index 259115f6a526a..0000000000000 --- a/src/core/public/styles/rendering/_base.scss +++ /dev/null @@ -1,80 +0,0 @@ -@import '../../mixins'; - -/** - * Stretch the root element of the Kibana application to set the base-size that - * flexed children should keep. Only works when paired with root styles applied - * by core service from new platform - */ - -#kibana-body { - // DO NOT ADD ANY OVERFLOW BEHAVIORS HERE - // It will break the sticky navigation - min-height: 100%; - display: flex; - flex-direction: column; -} - -// Affixes a div to restrict the position of charts tooltip to the visible viewport minus the header -#app-fixed-viewport { - pointer-events: none; - visibility: hidden; - position: fixed; - top: var(--kbnAppHeadersOffset, var(--euiFixedHeadersOffset, 0)); - right: 0; - bottom: 0; - left: 0; -} - -.kbnAppWrapper { - // DO NOT ADD ANY OTHER STYLES TO THIS SELECTOR - // This a very nested dependency happnening in "all" apps - display: flex; - flex-flow: column nowrap; - flex-grow: 1; - z-index: 0; // This effectively puts every high z-index inside the scope of this wrapper to it doesn't interfere with the header and/or overlay mask - position: relative; // This is temporary for apps that relied on this being present on `.application` -} - -.kbnBody { - padding-top: var(--euiFixedHeadersOffset, 0); -} - -// Conditionally override :root CSS fixed header variable. Updating `--euiFixedHeadersOffset` -// on the body will cause all child EUI components to automatically update their offsets -.kbnBody--hasHeaderBanner { - --euiFixedHeadersOffset: var(--kbnHeaderOffsetWithBanner); - - // Offset fixed EuiHeaders by the top banner - .euiHeader[data-fixed-header] { - margin-top: var(--kbnHeaderBannerHeight); - } - - // Prevent banners from covering full screen data grids - .euiDataGrid--fullScreen { - height: calc(100vh - var(--kbnHeaderBannerHeight)); - top: var(--kbnHeaderBannerHeight); - } -} - -// Set a body CSS variable for the app container to use - calculates the total -// height of all fixed headers + the sticky action menu toolbar -.kbnBody--hasProjectActionMenu { - --kbnAppHeadersOffset: calc(var(--kbnHeaderOffset) + var(--kbnProjectHeaderAppActionMenuHeight)); - - &.kbnBody--hasHeaderBanner { - --kbnAppHeadersOffset: calc(var(--kbnHeaderOffsetWithBanner) + var(--kbnProjectHeaderAppActionMenuHeight)); - } -} - -.kbnBody--chromeHidden { - // stylelint-disable-next-line length-zero-no-unit - --euiFixedHeadersOffset: 0px; - - &.kbnBody--hasHeaderBanner { - --euiFixedHeadersOffset: var(--kbnHeaderBannerHeight); - } - - &.kbnBody--hasProjectActionMenu { - --kbnAppHeadersOffset: var(--euiFixedHeadersOffset, 0); - } -} diff --git a/src/core/public/styles/rendering/_index.scss b/src/core/public/styles/rendering/_index.scss deleted file mode 100644 index c8567498b42ec..0000000000000 --- a/src/core/public/styles/rendering/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './base'; diff --git a/src/dev/stage_by_owner.ts b/src/dev/stage_by_owner.ts new file mode 100644 index 0000000000000..7987874da75fe --- /dev/null +++ b/src/dev/stage_by_owner.ts @@ -0,0 +1,96 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import simpleGit from 'simple-git'; + +import { run } from '@kbn/dev-cli-runner'; +import { getOwningTeamsForPath, getCodeOwnersEntries, CodeOwnersEntry } from '@kbn/code-owners'; +import { asyncForEach } from '@kbn/std'; +import { inspect } from 'util'; + +const git = simpleGit(); + +interface File { + path: string; + staged: boolean; +} + +// Function to get the list of changed files +const getChangedFiles = async (): Promise => { + const { staged, files } = await git.status(); + return files.map((file) => ({ path: file.path, staged: staged.includes(file.path) })); +}; + +run( + async ({ flags, log }) => { + const { + _: [owner], + } = flags; + + const changedFiles = await getChangedFiles(); + const owners: { staged: Record; unstaged: Record } = { + staged: {}, + unstaged: {}, + }; + + let codeOwnersEntries: CodeOwnersEntry[] = []; + + try { + codeOwnersEntries = getCodeOwnersEntries(); + } catch (e) { + log.error('CODEOWNERS cannot be read.'); + process.exit(1); + } + + const getOwners = (file: string) => { + const teams = getOwningTeamsForPath(file, codeOwnersEntries); + + if (teams.length === 0) { + log.warning(`No owner found for ${file}`); + return []; + } + + return teams; + }; + + for (const file of changedFiles) { + const fileOwners = getOwners(file.path); + + if (fileOwners) { + await asyncForEach(fileOwners, async (fileOwner) => { + const loc = file.staged ? 'staged' : 'unstaged'; + + owners[loc][fileOwner] = [ + ...(owners[loc][fileOwner] || []), + file.path + (fileOwners.length > 1 ? ` (+${fileOwners.length - 1})` : ''), + ]; + + if (owner && fileOwner === owner) { + await git.add(file.path); + log.info(`Staged ${file.path}`); + } + }); + } + } + + if (!owner) { + log.info(inspect(owners, { colors: true, depth: null })); + } + + log.info('Done.'); + }, + { + usage: 'node src/dev/stage_by_owner.ts [owner]', + description: ` + This script stages files based on the CODEOWNERS file. + If an owner is provided, it stages the files owned by that owner. + Otherwise, it outputs changed files, grouped by owner. + `, + } +); diff --git a/src/dev/tsconfig.json b/src/dev/tsconfig.json index 87473c1e79e82..0e2e8e94c629b 100644 --- a/src/dev/tsconfig.json +++ b/src/dev/tsconfig.json @@ -44,5 +44,6 @@ "@kbn/core-test-helpers-kbn-server", "@kbn/dev-proc-runner", "@kbn/core-i18n-server-internal", + "@kbn/code-owners", ] } diff --git a/src/platform/packages/shared/deeplinks/observability/locators/apm.ts b/src/platform/packages/shared/deeplinks/observability/locators/apm.ts new file mode 100644 index 0000000000000..64e446e883f1f --- /dev/null +++ b/src/platform/packages/shared/deeplinks/observability/locators/apm.ts @@ -0,0 +1,18 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { SerializableRecord } from '@kbn/utility-types'; + +export const TRANSACTION_DETAILS_BY_TRACE_ID_LOCATOR = 'TRANSACTION_DETAILS_BY_TRACE_ID_LOCATOR'; + +export interface TransactionDetailsByTraceIdLocatorParams extends SerializableRecord { + rangeFrom?: string; + rangeTo?: string; + traceId: string; +} diff --git a/src/platform/packages/shared/deeplinks/observability/locators/index.ts b/src/platform/packages/shared/deeplinks/observability/locators/index.ts index 5d45f66194b6d..3317df2268df3 100644 --- a/src/platform/packages/shared/deeplinks/observability/locators/index.ts +++ b/src/platform/packages/shared/deeplinks/observability/locators/index.ts @@ -7,6 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +export * from './apm'; export * from './dataset_quality'; export * from './dataset_quality_details'; export * from './logs_explorer'; diff --git a/x-pack/packages/observability/logs_overview/kibana.jsonc b/x-pack/packages/observability/logs_overview/kibana.jsonc index 34d8ac98a5253..1709e01f926ed 100644 --- a/x-pack/packages/observability/logs_overview/kibana.jsonc +++ b/x-pack/packages/observability/logs_overview/kibana.jsonc @@ -4,6 +4,6 @@ "owner": [ "@elastic/obs-ux-logs-team" ], - "group": "observability", - "visibility": "private" + "group": "platform", + "visibility": "shared" } diff --git a/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/instance_as_filter.test.ts b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/instance_as_filter.test.ts new file mode 100644 index 0000000000000..bd39257ac72e7 --- /dev/null +++ b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/instance_as_filter.test.ts @@ -0,0 +1,183 @@ +/* + * 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 { EntityV2 } from '@kbn/entities-schema'; +import { instanceAsFilter } from './instance_as_filter'; +import { readSourceDefinitions } from './source_definition'; +import { loggerMock } from '@kbn/logging-mocks'; +import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; +import { EntitySourceDefinition } from '../types'; +import { UnknownEntityType } from '../errors/unknown_entity_type'; +import { InvalidEntityInstance } from '../errors/invalid_entity_instance'; + +const readSourceDefinitionsMock = readSourceDefinitions as jest.Mock; +jest.mock('./source_definition', () => ({ + readSourceDefinitions: jest.fn(), +})); +const esClientMock = elasticsearchServiceMock.createClusterClient(); +const logger = loggerMock.create(); + +describe('instanceAsFilter', () => { + it('throws if no sources are found for the type', async () => { + const instance: EntityV2 = { + 'entity.type': 'my_type', + 'entity.id': 'whatever', + 'entity.display_name': 'Whatever', + }; + + const sources: EntitySourceDefinition[] = []; + readSourceDefinitionsMock.mockResolvedValue(sources); + + await expect(instanceAsFilter(instance, esClientMock, logger)).rejects.toThrowError( + UnknownEntityType + ); + }); + + it('throws if the instance cannot match any sources due to missing identity fields', async () => { + const instance: EntityV2 = { + 'entity.type': 'my_type', + 'entity.id': 'whatever', + 'entity.display_name': 'Whatever', + }; + + const sources: EntitySourceDefinition[] = [ + { + id: 'my_source', + type_id: 'my_type', + identity_fields: ['host.name'], + index_patterns: [], + metadata_fields: [], + filters: [], + }, + ]; + readSourceDefinitionsMock.mockResolvedValue(sources); + + await expect(instanceAsFilter(instance, esClientMock, logger)).rejects.toThrowError( + InvalidEntityInstance + ); + }); + + it('creates a single source filter for a single identity field', async () => { + const instance: EntityV2 = { + 'entity.type': 'my_type', + 'entity.id': 'whatever', + 'entity.display_name': 'Whatever', + 'host.name': 'my_host', + }; + + const sources: EntitySourceDefinition[] = [ + { + id: 'my_source', + type_id: 'my_type', + identity_fields: ['host.name'], + index_patterns: [], + metadata_fields: [], + filters: [], + }, + ]; + readSourceDefinitionsMock.mockResolvedValue(sources); + + await expect(instanceAsFilter(instance, esClientMock, logger)).resolves.toBe( + '(host.name: "my_host")' + ); + }); + + it('creates a single source filter for multiple identity field', async () => { + const instance: EntityV2 = { + 'entity.type': 'my_type', + 'entity.id': 'whatever', + 'entity.display_name': 'Whatever', + 'host.name': 'my_host', + 'host.os': 'my_os', + }; + + const sources: EntitySourceDefinition[] = [ + { + id: 'my_source', + type_id: 'my_type', + identity_fields: ['host.name', 'host.os'], + index_patterns: [], + metadata_fields: [], + filters: [], + }, + ]; + readSourceDefinitionsMock.mockResolvedValue(sources); + + await expect(instanceAsFilter(instance, esClientMock, logger)).resolves.toBe( + '(host.name: "my_host" AND host.os: "my_os")' + ); + }); + + it('creates multiple source filters for a single identity field', async () => { + const instance: EntityV2 = { + 'entity.type': 'my_type', + 'entity.id': 'whatever', + 'entity.display_name': 'Whatever', + 'host.name': 'my_host', + 'host.os': 'my_os', + }; + + const sources: EntitySourceDefinition[] = [ + { + id: 'my_source_host', + type_id: 'my_type', + identity_fields: ['host.name'], + index_patterns: [], + metadata_fields: [], + filters: [], + }, + { + id: 'my_source_os', + type_id: 'my_type', + identity_fields: ['host.os'], + index_patterns: [], + metadata_fields: [], + filters: [], + }, + ]; + readSourceDefinitionsMock.mockResolvedValue(sources); + + await expect(instanceAsFilter(instance, esClientMock, logger)).resolves.toBe( + '(host.name: "my_host") OR (host.os: "my_os")' + ); + }); + + it('creates multiple source filters for multiple identity field', async () => { + const instance: EntityV2 = { + 'entity.type': 'my_type', + 'entity.id': 'whatever', + 'entity.display_name': 'Whatever', + 'host.name': 'my_host', + 'host.os': 'my_os', + 'host.arch': 'my_arch', + }; + + const sources: EntitySourceDefinition[] = [ + { + id: 'my_source_host', + type_id: 'my_type', + identity_fields: ['host.name', 'host.arch'], + index_patterns: [], + metadata_fields: [], + filters: [], + }, + { + id: 'my_source_os', + type_id: 'my_type', + identity_fields: ['host.os', 'host.arch'], + index_patterns: [], + metadata_fields: [], + filters: [], + }, + ]; + readSourceDefinitionsMock.mockResolvedValue(sources); + + await expect(instanceAsFilter(instance, esClientMock, logger)).resolves.toBe( + '(host.name: "my_host" AND host.arch: "my_arch") OR (host.os: "my_os" AND host.arch: "my_arch")' + ); + }); +}); diff --git a/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/instance_as_filter.ts b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/instance_as_filter.ts new file mode 100644 index 0000000000000..c936277db8e25 --- /dev/null +++ b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/definitions/instance_as_filter.ts @@ -0,0 +1,56 @@ +/* + * 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 { EntityV2 } from '@kbn/entities-schema'; +import { Logger } from '@kbn/core/server'; +import { compact } from 'lodash'; +import { readSourceDefinitions } from './source_definition'; +import { InternalClusterClient } from '../types'; +import { UnknownEntityType } from '../errors/unknown_entity_type'; +import { InvalidEntityInstance } from '../errors/invalid_entity_instance'; + +export async function instanceAsFilter( + instance: EntityV2, + clusterClient: InternalClusterClient, + logger: Logger +) { + const sources = await readSourceDefinitions(clusterClient, logger, { + type: instance['entity.type'], + }); + + if (sources.length === 0) { + throw new UnknownEntityType(`No sources found for type ${instance['entity.type']}`); + } + + const sourceFilters = compact( + sources.map((source) => { + const { identity_fields: identityFields } = source; + + const instanceHasRequiredFields = identityFields.every((identityField) => + instance[identityField] ? true : false + ); + + if (!instanceHasRequiredFields) { + return undefined; + } + + const fieldFilters = identityFields.map( + (identityField) => `${identityField}: "${instance[identityField]}"` + ); + + return `(${fieldFilters.join(' AND ')})`; + }) + ); + + if (sourceFilters.length === 0) { + throw new InvalidEntityInstance( + `Entity ${instance['entity.id']} of type ${instance['entity.type']} is missing some identity fields, no sources could match` + ); + } + + return sourceFilters.join(' OR '); +} diff --git a/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/errors/invalid_entity_instance.ts b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/errors/invalid_entity_instance.ts new file mode 100644 index 0000000000000..12b0a94fe3ebb --- /dev/null +++ b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/errors/invalid_entity_instance.ts @@ -0,0 +1,13 @@ +/* + * 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 class InvalidEntityInstance extends Error { + constructor(message: string) { + super(message); + this.name = 'InvalidEntityInstance'; + } +} diff --git a/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/queries/entity_instances.test.ts b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/queries/entity_instances.test.ts index e7fa8882bfcb9..8836b7635ff36 100644 --- a/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/queries/entity_instances.test.ts +++ b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/queries/entity_instances.test.ts @@ -28,7 +28,7 @@ describe('getEntityInstancesQuery', () => { expect(query).toEqual( 'FROM logs-*, metrics-* | ' + - 'STATS host.name = TOP(host.name::keyword, 10, "ASC"), entity.last_seen_timestamp = MAX(custom_timestamp_field), service.id = MAX(service.id::keyword) BY service.name::keyword | ' + + 'STATS host.name = VALUES(host.name::keyword), entity.last_seen_timestamp = MAX(custom_timestamp_field), service.id = MAX(service.id::keyword) BY service.name::keyword | ' + 'RENAME `service.name::keyword` AS service.name | ' + 'EVAL entity.type = "service", entity.id = service.name, entity.display_name = COALESCE(service.id, entity.id) | ' + 'SORT entity.id DESC | ' + diff --git a/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/queries/entity_instances.ts b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/queries/entity_instances.ts index dc79d815abd37..c9a5948b55dc1 100644 --- a/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/queries/entity_instances.ts +++ b/x-pack/platform/plugins/shared/entity_manager/server/lib/v2/queries/entity_instances.ts @@ -46,7 +46,7 @@ const dslFilter = ({ const statsCommand = ({ source }: { source: EntitySourceDefinition }) => { const aggs = source.metadata_fields .filter((field) => !source.identity_fields.some((idField) => idField === field)) - .map((field) => `${field} = TOP(${asKeyword(field)}, 10, "ASC")`); + .map((field) => `${field} = VALUES(${asKeyword(field)})`); if (source.timestamp_field) { aggs.push(`entity.last_seen_timestamp = MAX(${source.timestamp_field})`); diff --git a/x-pack/platform/plugins/shared/entity_manager/server/plugin.ts b/x-pack/platform/plugins/shared/entity_manager/server/plugin.ts index fed5b1c4df458..8799c7f365bf7 100644 --- a/x-pack/platform/plugins/shared/entity_manager/server/plugin.ts +++ b/x-pack/platform/plugins/shared/entity_manager/server/plugin.ts @@ -40,11 +40,15 @@ import { READ_ENTITIES_PRIVILEGE, } from './lib/v2/constants'; import { installBuiltInDefinitions } from './lib/v2/definitions/install_built_in_definitions'; +import { instanceAsFilter } from './lib/v2/definitions/instance_as_filter'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface EntityManagerServerPluginSetup {} export interface EntityManagerServerPluginStart { getScopedClient: (options: { request: KibanaRequest }) => Promise; + v2: { + instanceAsFilter: typeof instanceAsFilter; + }; } export const config: PluginConfigDescriptor = { @@ -197,6 +201,9 @@ export class EntityManagerServerPlugin getScopedClient: async ({ request }: { request: KibanaRequest }) => { return this.getScopedClient({ request, coreStart: core }); }, + v2: { + instanceAsFilter, + }, }; } diff --git a/x-pack/platform/plugins/shared/entity_manager/tsconfig.json b/x-pack/platform/plugins/shared/entity_manager/tsconfig.json index beb8097502b2b..0fc46870ea472 100644 --- a/x-pack/platform/plugins/shared/entity_manager/tsconfig.json +++ b/x-pack/platform/plugins/shared/entity_manager/tsconfig.json @@ -38,5 +38,6 @@ "@kbn/es-types", "@kbn/apm-utils", "@kbn/features-plugin", + "@kbn/core-elasticsearch-server-mocks", ] } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_badges.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_badges.tsx index 44363d12088d1..21ff0e22505bf 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_badges.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_badges.tsx @@ -5,7 +5,13 @@ * 2.0. */ -import { EuiFlexGroup, EuiHealth, EuiNotificationBadge, EuiFlexItem } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiHealth, + EuiNotificationBadge, + EuiFlexItem, + useEuiTheme, +} from '@elastic/eui'; import React, { memo } from 'react'; import { @@ -31,9 +37,10 @@ export const AgentStatusBadges: React.FC<{ const AgentStatusBadge: React.FC<{ status: SimplifiedAgentStatus; count: number }> = memo( ({ status, count }) => { + const { euiTheme } = useEuiTheme(); return ( <> - + {getLabelForAgentStatus(status)} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_bar.tsx index f4fc01204267b..429702e6b7dc6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_bar.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/status_bar.tsx @@ -6,7 +6,7 @@ */ import styled from 'styled-components'; -import { EuiColorPaletteDisplay, EuiSpacer } from '@elastic/eui'; +import { EuiColorPaletteDisplay, EuiSpacer, useEuiTheme } from '@elastic/eui'; import React, { useMemo } from 'react'; import { AGENT_STATUSES, getColorForAgentStatus } from '../../services/agent_status'; @@ -25,16 +25,17 @@ const StyledEuiColorPaletteDisplay = styled(EuiColorPaletteDisplay)` export const AgentStatusBar: React.FC<{ agentStatus: { [k in SimplifiedAgentStatus]: number }; }> = ({ agentStatus }) => { + const { euiTheme } = useEuiTheme(); const palette = useMemo(() => { return AGENT_STATUSES.reduce((acc, status) => { const previousStop = acc.length > 0 ? acc[acc.length - 1].stop : 0; acc.push({ stop: previousStop + (agentStatus[status] || 0), - color: getColorForAgentStatus(status), + color: getColorForAgentStatus(status, euiTheme), }); return acc; }, [] as Array<{ stop: number; color: string }>); - }, [agentStatus]); + }, [agentStatus, euiTheme]); const hasNoAgent = palette[palette.length - 1].stop === 0; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx index b69da08105759..6b12331d7034c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx @@ -5,24 +5,11 @@ * 2.0. */ -import { euiPaletteColorBlindBehindText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { euiLightVars } from '@kbn/ui-theme'; -import type { SimplifiedAgentStatus } from '../../../types'; +import type { EuiThemeComputed } from '@elastic/eui-theme-common'; -const visColors = euiPaletteColorBlindBehindText(); -const colorToHexMap = { - // using variables as mentioned here https://elastic.github.io/eui/#/guidelines/getting-started - default: euiLightVars.euiColorLightShade, - primary: visColors[1], - success: visColors[0], - accent: visColors[2], - warning: visColors[5], - danger: visColors[9], - inactive: euiLightVars.euiColorDarkShade, - lightest: euiLightVars.euiColorDisabled, -}; +import type { SimplifiedAgentStatus } from '../../../types'; export const AGENT_STATUSES: SimplifiedAgentStatus[] = [ 'healthy', @@ -33,20 +20,23 @@ export const AGENT_STATUSES: SimplifiedAgentStatus[] = [ 'unenrolled', ]; -export function getColorForAgentStatus(agentStatus: SimplifiedAgentStatus): string { +export function getColorForAgentStatus( + agentStatus: SimplifiedAgentStatus, + euiTheme: EuiThemeComputed<{}> +): string { switch (agentStatus) { case 'healthy': - return colorToHexMap.success; + return euiTheme.colors.backgroundFilledSuccess; case 'offline': - return colorToHexMap.default; + return euiTheme.colors.lightShade; case 'inactive': - return colorToHexMap.inactive; + return euiTheme.colors.darkShade; case 'unhealthy': - return colorToHexMap.warning; + return euiTheme.colors.backgroundFilledWarning; case 'updating': - return colorToHexMap.primary; + return euiTheme.colors.backgroundFilledPrimary; case 'unenrolled': - return colorToHexMap.lightest; + return euiTheme.colors.backgroundBaseDisabled; default: throw new Error(`Unsupported Agent status ${agentStatus}`); } diff --git a/x-pack/plugins/observability_solution/apm/kibana.jsonc b/x-pack/plugins/observability_solution/apm/kibana.jsonc index bcb0801fc6394..0b8e8bd70a9a6 100644 --- a/x-pack/plugins/observability_solution/apm/kibana.jsonc +++ b/x-pack/plugins/observability_solution/apm/kibana.jsonc @@ -39,6 +39,7 @@ "uiActions", "logsDataAccess", "savedSearch", + "entityManager" ], "optionalPlugins": [ "actions", diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/entities/entity_link/entity_link.test.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/entities/entity_link/entity_link.test.tsx index cdf6f23eb53d9..4515c2cdd5714 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/entities/entity_link/entity_link.test.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/entities/entity_link/entity_link.test.tsx @@ -173,7 +173,9 @@ describe('Entity link', () => { renderEntityLink({ isEntityCentricExperienceEnabled: true, serviceEntitySummaryMockReturnValue: { - serviceEntitySummary: { dataStreamTypes: ['metrics'] } as unknown as ServiceEntitySummary, + serviceEntitySummary: { + ['data_stream.type']: ['metrics'], + } as unknown as ServiceEntitySummary, serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS, }, hasApmDataFetcherMockReturnValue: { @@ -200,7 +202,9 @@ describe('Entity link', () => { renderEntityLink({ isEntityCentricExperienceEnabled: true, serviceEntitySummaryMockReturnValue: { - serviceEntitySummary: { dataStreamTypes: ['metrics'] } as unknown as ServiceEntitySummary, + serviceEntitySummary: { + ['data_stream.type']: ['metrics'], + } as unknown as ServiceEntitySummary, serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS, }, hasApmDataFetcherMockReturnValue: { diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/get_entities.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/get_entities.ts deleted file mode 100644 index 6cedb09efa7c2..0000000000000 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/get_entities.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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 type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { - ENTITY_FIRST_SEEN, - ENTITY_LAST_SEEN, -} from '@kbn/observability-shared-plugin/common/field_names/elasticsearch'; -import type { EntitiesESClient } from '../../lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client'; -import { getEntityLatestServices } from './get_entity_latest_services'; -import type { EntityLatestServiceRaw } from './types'; - -export function entitiesRangeQuery(start?: number, end?: number): QueryDslQueryContainer[] { - if (!start || !end) { - return []; - } - - return [ - { - range: { - [ENTITY_LAST_SEEN]: { - gte: start, - }, - }, - }, - { - range: { - [ENTITY_FIRST_SEEN]: { - lte: end, - }, - }, - }, - ]; -} - -export async function getEntities({ - entitiesESClient, - start, - end, - environment, - kuery, - size, - serviceName, -}: { - entitiesESClient: EntitiesESClient; - start: number; - end: number; - environment: string; - kuery?: string; - size: number; - serviceName?: string; -}): Promise { - const entityLatestServices = await getEntityLatestServices({ - entitiesESClient, - start, - end, - environment, - kuery, - size, - serviceName, - }); - - return entityLatestServices; -} diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/get_entity_latest_services.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/get_entity_latest_services.ts deleted file mode 100644 index e08be75072b6f..0000000000000 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/get_entity_latest_services.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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, termQuery } from '@kbn/observability-plugin/server'; -import { - ENTITY, - ENTITY_TYPE, - SOURCE_DATA_STREAM_TYPE, -} from '@kbn/observability-shared-plugin/common/field_names/elasticsearch'; -import { AGENT_NAME, SERVICE_ENVIRONMENT, SERVICE_NAME } from '../../../common/es_fields/apm'; -import { environmentQuery } from '../../../common/utils/environment_query'; -import { EntitiesESClient } from '../../lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client'; -import { entitiesRangeQuery } from './get_entities'; -import { EntityLatestServiceRaw, EntityType } from './types'; - -export async function getEntityLatestServices({ - entitiesESClient, - start, - end, - environment, - kuery, - size, - serviceName, -}: { - entitiesESClient: EntitiesESClient; - start?: number; - end?: number; - environment: string; - kuery?: string; - size: number; - serviceName?: string; -}): Promise { - const latestEntityServices = ( - await entitiesESClient.searchLatest(`get_entity_latest_services`, { - body: { - size, - track_total_hits: false, - _source: [AGENT_NAME, ENTITY, SOURCE_DATA_STREAM_TYPE, SERVICE_NAME, SERVICE_ENVIRONMENT], - query: { - bool: { - filter: [ - ...kqlQuery(kuery), - ...environmentQuery(environment, SERVICE_ENVIRONMENT), - ...entitiesRangeQuery(start, end), - ...termQuery(ENTITY_TYPE, EntityType.SERVICE), - ...termQuery(SERVICE_NAME, serviceName), - ], - }, - }, - }, - }) - ).hits.hits.map((hit) => hit._source); - - return latestEntityServices; -} diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/services/get_service_entities.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/services/get_service_entities.ts deleted file mode 100644 index 9e6bb34bceafe..0000000000000 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/services/get_service_entities.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 { errors } from '@elastic/elasticsearch'; -import { Logger } from '@kbn/core/server'; -import { WrappedElasticsearchClientError } from '@kbn/observability-plugin/server'; -import { EntitiesESClient } from '../../../lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client'; -import { withApmSpan } from '../../../utils/with_apm_span'; -import { getEntities } from '../get_entities'; -import { mergeEntities } from '../utils/merge_entities'; - -export const MAX_NUMBER_OF_SERVICES = 1_000; - -export async function getServiceEntities({ - entitiesESClient, - start, - end, - kuery, - environment, - logger, -}: { - entitiesESClient: EntitiesESClient; - start: number; - end: number; - kuery: string; - environment: string; - logger: Logger; -}) { - return withApmSpan('get_service_entities', async () => { - try { - const entities = await getEntities({ - entitiesESClient, - start, - end, - kuery, - environment, - size: MAX_NUMBER_OF_SERVICES, - }); - - return mergeEntities({ entities }); - } catch (error) { - // If the index does not exist, handle it gracefully - if ( - error instanceof WrappedElasticsearchClientError && - error.originalError instanceof errors.ResponseError - ) { - const type = error.originalError.body.error.type; - - if (type === 'index_not_found_exception') { - logger.error(`Entities index does not exist. Unable to fetch services.`); - return []; - } - } - - throw error; - } - }); -} diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/services/get_service_entity_summary.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/services/get_service_entity_summary.ts index 3ab3b907f5be2..90a44a9b609e6 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/services/get_service_entity_summary.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/entities/services/get_service_entity_summary.ts @@ -5,28 +5,31 @@ * 2.0. */ -import type { EntitiesESClient } from '../../../lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client'; +import { SERVICE_NAME, AGENT_NAME, SERVICE_ENVIRONMENT } from '@kbn/apm-types'; +import { BUILT_IN_ENTITY_TYPES, DATA_STREAM_TYPE } from '@kbn/observability-shared-plugin/common'; +import moment from 'moment'; +import type { EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client'; import { withApmSpan } from '../../../utils/with_apm_span'; -import { getEntityLatestServices } from '../get_entity_latest_services'; import { mergeEntities } from '../utils/merge_entities'; -import { MAX_NUMBER_OF_SERVICES } from './get_service_entities'; interface Params { - entitiesESClient: EntitiesESClient; + entityManagerClient: EntityClient; serviceName: string; environment: string; } -export function getServiceEntitySummary({ entitiesESClient, environment, serviceName }: Params) { +export function getServiceEntitySummary({ entityManagerClient, environment, serviceName }: Params) { return withApmSpan('get_service_entity_summary', async () => { - const entityLatestServices = await getEntityLatestServices({ - entitiesESClient, - environment, - size: MAX_NUMBER_OF_SERVICES, - serviceName, + const serviceEntitySummary = await entityManagerClient.v2.searchEntities({ + start: moment().subtract(15, 'm').toISOString(), + end: moment().toISOString(), + type: BUILT_IN_ENTITY_TYPES.SERVICE_V2, + filters: [`${SERVICE_NAME}: "${serviceName}"`], + limit: 1, + metadata_fields: [DATA_STREAM_TYPE, AGENT_NAME, SERVICE_ENVIRONMENT], }); - const serviceEntity = mergeEntities({ entities: entityLatestServices }); + const serviceEntity = mergeEntities({ entities: serviceEntitySummary?.entities }); return serviceEntity[0]; }); } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/services/routes.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/services/routes.ts index ab89f9417ec88..e3d44645ad394 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/services/routes.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/entities/services/routes.ts @@ -6,10 +6,8 @@ */ import * as t from 'io-ts'; import { environmentQuery } from '../../../../common/utils/environment_query'; -import { createEntitiesESClient } from '../../../lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client'; import { createApmServerRoute } from '../../apm_routes/create_apm_server_route'; import { environmentRt, kueryRt, rangeRt } from '../../default_api_types'; -import { getServiceEntities } from './get_service_entities'; import { getServiceEntitySummary } from './get_service_entity_summary'; const serviceEntitiesSummaryRoute = createApmServerRoute({ @@ -20,52 +18,20 @@ const serviceEntitiesSummaryRoute = createApmServerRoute({ }), security: { authz: { requiredPrivileges: ['apm'] } }, async handler(resources) { - const { context, params, request } = resources; - const coreContext = await context.core; - - const entitiesESClient = await createEntitiesESClient({ - request, - esClient: coreContext.elasticsearch.client.asCurrentUser, - }); + const { params, request, plugins } = resources; + const entityManagerStart = await plugins.entityManager.start(); + const entityManagerClient = await entityManagerStart.getScopedClient({ request }); const { serviceName } = params.path; const { environment } = params.query; - return getServiceEntitySummary({ - entitiesESClient, + const serviceEntitySummary = await getServiceEntitySummary({ + entityManagerClient, serviceName, environment, }); - }, -}); - -const servicesEntitiesRoute = createApmServerRoute({ - endpoint: 'GET /internal/apm/entities/services', - params: t.type({ - query: t.intersection([environmentRt, kueryRt, rangeRt]), - }), - security: { authz: { requiredPrivileges: ['apm'] } }, - async handler(resources) { - const { context, params, request } = resources; - const coreContext = await context.core; - - const entitiesESClient = await createEntitiesESClient({ - request, - esClient: coreContext.elasticsearch.client.asCurrentUser, - }); - - const { start, end, kuery, environment } = params.query; - - const services = await getServiceEntities({ - entitiesESClient, - start, - end, - kuery, - environment, - logger: resources.logger, - }); - return { services }; + return serviceEntitySummary; }, }); @@ -137,7 +103,6 @@ const serviceLogErrorRateTimeseriesRoute = createApmServerRoute({ }); export const servicesEntitiesRoutesRepository = { - ...servicesEntitiesRoute, ...serviceLogRateTimeseriesRoute, ...serviceLogErrorRateTimeseriesRoute, ...serviceEntitiesSummaryRoute, diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/types.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/types.ts index 833e565ec00ef..c65af1a05a26a 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/types.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/entities/types.ts @@ -4,28 +4,10 @@ * 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 type { EntityMetadata, EntityV2 } from '@kbn/entities-schema'; export enum EntityType { SERVICE = 'service', } -export interface EntityLatestServiceRaw { - agent: { - name: AgentName[]; - }; - source_data_stream: { - type: string[]; - }; - service: { - name: string; - environment?: string; - }; - entity: Entity; -} - -interface Entity { - id: string; - last_seen_timestamp: string; - identity_fields: string[]; -} +export type EntityLatestServiceRaw = EntityV2 & EntityMetadata; diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/merge_entities.test.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/merge_entities.test.ts index e3dd0ef5e0d4e..91f1eff244def 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/merge_entities.test.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/merge_entities.test.ts @@ -13,17 +13,14 @@ describe('mergeEntities', () => { it('modifies one service', () => { const entities: EntityLatestServiceRaw[] = [ { - service: { - name: 'service-1', - environment: 'test', - }, - agent: { name: ['nodejs'] }, - source_data_stream: { type: ['metrics', 'logs'] }, - entity: { - last_seen_timestamp: '2024-06-05T10:34:40.810Z', - identity_fields: ['service.name', 'service.environment'], - id: 'service-1:test', - }, + 'data_stream.type': ['metrics', 'logs'], + 'agent.name': 'nodejs', + 'service.environment': 'test', + 'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z', + 'service.name': 'service-1', + 'entity.type': 'built_in_services_from_ecs_data', + 'entity.id': 'service-1', + 'entity.display_name': 'service-1', }, ]; const result = mergeEntities({ entities }); @@ -32,7 +29,7 @@ describe('mergeEntities', () => { agentName: 'nodejs' as AgentName, dataStreamTypes: ['metrics', 'logs'], environments: ['test'], - lastSeenTimestamp: '2024-06-05T10:34:40.810Z', + lastSeenTimestamp: '2024-12-13T14:52:35.461Z', serviceName: 'service-1', }, ]); @@ -41,56 +38,44 @@ describe('mergeEntities', () => { it('joins two service with the same name ', () => { const entities: EntityLatestServiceRaw[] = [ { - service: { - name: 'service-1', - environment: 'env-service-1', - }, - agent: { name: ['nodejs'] }, - source_data_stream: { type: ['foo'] }, - entity: { - last_seen_timestamp: '2024-03-05T10:34:40.810Z', - identity_fields: ['service.name', 'service.environment'], - id: 'service-1:env-service-1', - }, + 'data_stream.type': ['foo'], + 'agent.name': 'nodejs', + 'service.environment': 'env-service-1', + 'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z', + 'service.name': 'service-1', + 'entity.type': 'built_in_services_from_ecs_data', + 'entity.id': 'service-1:env-service-1', + 'entity.display_name': 'service-1', }, { - service: { - name: 'service-1', - environment: 'env-service-2', - }, - agent: { name: ['nodejs'] }, - source_data_stream: { type: ['bar'] }, - entity: { - last_seen_timestamp: '2024-03-05T10:34:40.810Z', - identity_fields: ['service.name', 'service.environment'], - id: 'apm-only-1:synthtrace-env-2', - }, + 'data_stream.type': ['bar'], + 'agent.name': 'nodejs', + 'service.environment': 'env-service-2', + 'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z', + 'service.name': 'service-1', + 'entity.type': 'built_in_services_from_ecs_data', + 'entity.id': 'service-1:env-service-2', + 'entity.display_name': 'service-1', }, { - service: { - name: 'service-2', - environment: 'env-service-3', - }, - agent: { name: ['java'] }, - source_data_stream: { type: ['baz'] }, - entity: { - last_seen_timestamp: '2024-06-05T10:34:40.810Z', - identity_fields: ['service.name', 'service.environment'], - id: 'service-2:env-service-3', - }, + 'data_stream.type': ['baz'], + 'agent.name': 'java', + 'service.environment': 'env-service-3', + 'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z', + 'service.name': 'service-2', + 'entity.type': 'built_in_services_from_ecs_data', + 'entity.id': 'service-2:env-service-3', + 'entity.display_name': 'service-2', }, { - service: { - name: 'service-2', - environment: 'env-service-4', - }, - agent: { name: ['java'] }, - source_data_stream: { type: ['baz'] }, - entity: { - last_seen_timestamp: '2024-06-05T10:34:40.810Z', - identity_fields: ['service.name', 'service.environment'], - id: 'service-2:env-service-3', - }, + 'data_stream.type': ['baz'], + 'agent.name': ['java'], + 'service.environment': 'env-service-4', + 'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z', + 'service.name': 'service-2', + 'entity.type': 'built_in_services_from_ecs_data', + 'entity.id': 'service-2:env-service-4', + 'entity.display_name': 'service-2', }, ]; @@ -100,14 +85,14 @@ describe('mergeEntities', () => { agentName: 'nodejs' as AgentName, dataStreamTypes: ['foo', 'bar'], environments: ['env-service-1', 'env-service-2'], - lastSeenTimestamp: '2024-03-05T10:34:40.810Z', + lastSeenTimestamp: '2024-12-13T14:52:35.461Z', serviceName: 'service-1', }, { agentName: 'java' as AgentName, dataStreamTypes: ['baz'], environments: ['env-service-3', 'env-service-4'], - lastSeenTimestamp: '2024-06-05T10:34:40.810Z', + lastSeenTimestamp: '2024-12-13T14:52:35.461Z', serviceName: 'service-2', }, ]); @@ -115,43 +100,34 @@ describe('mergeEntities', () => { it('handles duplicate environments and data streams', () => { const entities: EntityLatestServiceRaw[] = [ { - service: { - name: 'service-1', - environment: 'test', - }, - agent: { name: ['nodejs'] }, - source_data_stream: { type: ['metrics', 'logs'] }, - entity: { - last_seen_timestamp: '2024-06-05T10:34:40.810Z', - identity_fields: ['service.name', 'service.environment'], - id: 'service-1:test', - }, + 'data_stream.type': ['metrics', 'logs'], + 'agent.name': ['nodejs'], + 'service.environment': 'test', + 'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z', + 'service.name': 'service-1', + 'entity.type': 'built_in_services_from_ecs_data', + 'entity.id': 'service-1:test', + 'entity.display_name': 'service-1', }, { - service: { - name: 'service-1', - environment: 'test', - }, - agent: { name: ['nodejs'] }, - source_data_stream: { type: ['metrics', 'logs'] }, - entity: { - last_seen_timestamp: '2024-06-05T10:34:40.810Z', - identity_fields: ['service.name', 'service.environment'], - id: 'service-1:test', - }, + 'data_stream.type': ['metrics', 'logs'], + 'agent.name': ['nodejs'], + 'service.environment': 'test', + 'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z', + 'service.name': 'service-1', + 'entity.type': 'built_in_services_from_ecs_data', + 'entity.id': 'service-1:test', + 'entity.display_name': 'service-1', }, { - service: { - name: 'service-1', - environment: 'prod', - }, - agent: { name: ['nodejs'] }, - source_data_stream: { type: ['foo'] }, - entity: { - last_seen_timestamp: '2024-23-05T10:34:40.810Z', - identity_fields: ['service.name', 'service.environment'], - id: 'service-1:prod', - }, + 'data_stream.type': ['foo'], + 'agent.name': ['nodejs'], + 'service.environment': 'prod', + 'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z', + 'service.name': 'service-1', + 'entity.type': 'built_in_services_from_ecs_data', + 'entity.id': 'service-1:prod', + 'entity.display_name': 'service-1', }, ]; const result = mergeEntities({ entities }); @@ -160,7 +136,7 @@ describe('mergeEntities', () => { agentName: 'nodejs' as AgentName, dataStreamTypes: ['metrics', 'logs', 'foo'], environments: ['test', 'prod'], - lastSeenTimestamp: '2024-23-05T10:34:40.810Z', + lastSeenTimestamp: '2024-12-13T14:52:35.461Z', serviceName: 'service-1', }, ]); @@ -168,17 +144,14 @@ describe('mergeEntities', () => { it('handles null environment', () => { const entity: EntityLatestServiceRaw[] = [ { - service: { - name: 'service-1', - environment: undefined, - }, - agent: { name: ['nodejs'] }, - source_data_stream: { type: [] }, - entity: { - last_seen_timestamp: '2024-06-05T10:34:40.810Z', - identity_fields: ['service.name'], - id: 'service-1:test', - }, + 'data_stream.type': [], + 'agent.name': ['nodejs'], + 'service.environment': null, + 'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z', + 'service.name': 'service-1', + 'entity.type': 'built_in_services_from_ecs_data', + 'entity.id': 'service-1:test', + 'entity.display_name': 'service-1', }, ]; const entityResult = mergeEntities({ entities: entity }); @@ -187,35 +160,31 @@ describe('mergeEntities', () => { agentName: 'nodejs' as AgentName, dataStreamTypes: [], environments: [], - lastSeenTimestamp: '2024-06-05T10:34:40.810Z', + lastSeenTimestamp: '2024-12-13T14:52:35.461Z', serviceName: 'service-1', }, ]); const entities: EntityLatestServiceRaw[] = [ { - service: { - name: 'service-1', - }, - agent: { name: ['nodejs'] }, - source_data_stream: { type: [] }, - entity: { - last_seen_timestamp: '2024-06-05T10:34:40.810Z', - identity_fields: ['service.name'], - id: 'service-1:test', - }, + 'data_stream.type': [], + 'agent.name': ['nodejs'], + 'service.environment': null, + 'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z', + 'service.name': 'service-1', + 'entity.type': 'built_in_services_from_ecs_data', + 'entity.id': 'service-1:test', + 'entity.display_name': 'service-1', }, { - service: { - name: 'service-1', - }, - agent: { name: ['nodejs'] }, - source_data_stream: { type: [] }, - entity: { - last_seen_timestamp: '2024-06-05T10:34:40.810Z', - identity_fields: ['service.name'], - id: 'service-1:test', - }, + 'data_stream.type': [], + 'agent.name': ['nodejs'], + 'service.environment': null, + 'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z', + 'service.name': 'service-1', + 'entity.type': 'built_in_services_from_ecs_data', + 'entity.id': 'service-1:test', + 'entity.display_name': 'service-1', }, ]; const result = mergeEntities({ entities }); @@ -224,7 +193,7 @@ describe('mergeEntities', () => { agentName: 'nodejs' as AgentName, dataStreamTypes: [], environments: [], - lastSeenTimestamp: '2024-06-05T10:34:40.810Z', + lastSeenTimestamp: '2024-12-13T14:52:35.461Z', serviceName: 'service-1', }, ]); @@ -233,16 +202,13 @@ describe('mergeEntities', () => { it('handles undefined environment', () => { const entity: EntityLatestServiceRaw[] = [ { - service: { - name: 'service-1', - }, - agent: { name: ['nodejs'] }, - source_data_stream: { type: [] }, - entity: { - last_seen_timestamp: '2024-06-05T10:34:40.810Z', - identity_fields: ['service.name'], - id: 'service-1:test', - }, + 'data_stream.type': [], + 'agent.name': ['nodejs'], + 'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z', + 'service.name': 'service-1', + 'entity.type': 'built_in_services_from_ecs_data', + 'entity.id': 'service-1:test', + 'entity.display_name': 'service-1', }, ]; const entityResult = mergeEntities({ entities: entity }); @@ -251,35 +217,29 @@ describe('mergeEntities', () => { agentName: 'nodejs', dataStreamTypes: [], environments: [], - lastSeenTimestamp: '2024-06-05T10:34:40.810Z', + lastSeenTimestamp: '2024-12-13T14:52:35.461Z', serviceName: 'service-1', }, ]); const entities: EntityLatestServiceRaw[] = [ { - service: { - name: 'service-1', - }, - agent: { name: ['nodejs'] }, - source_data_stream: { type: [] }, - entity: { - last_seen_timestamp: '2024-06-05T10:34:40.810Z', - identity_fields: ['service.name'], - id: 'service-1:test', - }, + 'data_stream.type': [], + 'agent.name': ['nodejs'], + 'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z', + 'service.name': 'service-1', + 'entity.type': 'built_in_services_from_ecs_data', + 'entity.id': 'service-1:test', + 'entity.display_name': 'service-1', }, { - service: { - name: 'service-1', - }, - agent: { name: ['nodejs'] }, - source_data_stream: { type: [] }, - entity: { - last_seen_timestamp: '2024-06-05T10:34:40.810Z', - identity_fields: ['service.name'], - id: 'service-1:test', - }, + 'data_stream.type': [], + 'agent.name': ['nodejs'], + 'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z', + 'service.name': 'service-1', + 'entity.type': 'built_in_services_from_ecs_data', + 'entity.id': 'service-1:test', + 'entity.display_name': 'service-1', }, ]; const result = mergeEntities({ entities }); @@ -288,7 +248,7 @@ describe('mergeEntities', () => { agentName: 'nodejs', dataStreamTypes: [], environments: [], - lastSeenTimestamp: '2024-06-05T10:34:40.810Z', + lastSeenTimestamp: '2024-12-13T14:52:35.461Z', serviceName: 'service-1', }, ]); @@ -297,17 +257,14 @@ describe('mergeEntities', () => { it('has no logs when log rate is not returned', () => { const entities: EntityLatestServiceRaw[] = [ { - service: { - name: 'service-1', - environment: 'test', - }, - agent: { name: ['nodejs'] }, - source_data_stream: { type: ['metrics'] }, - entity: { - last_seen_timestamp: '2024-06-05T10:34:40.810Z', - identity_fields: ['service.name', 'service.environment'], - id: 'service-1:test', - }, + 'data_stream.type': ['metrics'], + 'agent.name': ['nodejs'], + 'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z', + 'service.name': 'service-1', + 'service.environment': 'test', + 'entity.type': 'built_in_services_from_ecs_data', + 'entity.id': 'service-1:test', + 'entity.display_name': 'service-1', }, ]; const result = mergeEntities({ entities }); @@ -316,7 +273,31 @@ describe('mergeEntities', () => { agentName: 'nodejs' as AgentName, dataStreamTypes: ['metrics'], environments: ['test'], - lastSeenTimestamp: '2024-06-05T10:34:40.810Z', + lastSeenTimestamp: '2024-12-13T14:52:35.461Z', + serviceName: 'service-1', + }, + ]); + }); + it('has multiple duplicate environments and data stream types', () => { + const entities: EntityLatestServiceRaw[] = [ + { + 'data_stream.type': ['metrics', 'metrics', 'logs', 'logs'], + 'agent.name': ['nodejs', 'nodejs'], + 'entity.last_seen_timestamp': '2024-12-13T14:52:35.461Z', + 'service.name': 'service-1', + 'service.environment': ['test', 'test', 'test'], + 'entity.type': 'built_in_services_from_ecs_data', + 'entity.id': 'service-1:test', + 'entity.display_name': 'service-1', + }, + ]; + const result = mergeEntities({ entities }); + expect(result).toEqual([ + { + agentName: 'nodejs' as AgentName, + dataStreamTypes: ['metrics', 'logs'], + environments: ['test'], + lastSeenTimestamp: '2024-12-13T14:52:35.461Z', serviceName: 'service-1', }, ]); diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/merge_entities.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/merge_entities.ts index 2f33c4728bd1a..1e95656cb1f8e 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/merge_entities.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/merge_entities.ts @@ -23,7 +23,7 @@ export function mergeEntities({ entities: EntityLatestServiceRaw[]; }): MergedServiceEntity[] { const mergedEntities = entities.reduce((map, current) => { - const key = current.service.name; + const key = current['service.name']; if (map.has(key)) { const existingEntity = map.get(key); map.set(key, mergeFunc(current, existingEntity)); @@ -33,28 +33,37 @@ export function mergeEntities({ return map; }, new Map()); - return [...mergedEntities.values()]; + return [...new Set(mergedEntities.values())]; } function mergeFunc(entity: EntityLatestServiceRaw, existingEntity?: MergedServiceEntity) { const commonEntityFields = { - serviceName: entity.service.name, - agentName: entity.agent.name[0], - lastSeenTimestamp: entity.entity.last_seen_timestamp, + serviceName: entity['service.name'], + agentName: + Array.isArray(entity['agent.name']) && entity['agent.name'].length > 0 + ? entity['agent.name'][0] + : entity['agent.name'], + lastSeenTimestamp: entity['entity.last_seen_timestamp'], }; if (!existingEntity) { return { ...commonEntityFields, - dataStreamTypes: entity.source_data_stream.type, - environments: compact([entity?.service.environment]), + dataStreamTypes: uniq(entity['data_stream.type']), + environments: uniq( + compact( + Array.isArray(entity['service.environment']) + ? entity['service.environment'] + : [entity['service.environment']] + ) + ), }; } return { ...commonEntityFields, dataStreamTypes: uniq( - compact([...(existingEntity?.dataStreamTypes ?? []), ...entity.source_data_stream.type]) + compact([...(existingEntity?.dataStreamTypes ?? []), ...entity['data_stream.type']]) ), - environments: uniq(compact([...existingEntity?.environments, entity?.service.environment])), + environments: uniq(compact([...existingEntity?.environments, entity['service.environment']])), }; } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/services/route.ts b/x-pack/plugins/observability_solution/apm/server/routes/services/route.ts index d6c3b5b73e3d8..71d570d2708f7 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/services/route.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/services/route.ts @@ -80,7 +80,6 @@ import { import { getThroughput, ServiceThroughputResponse } from './get_throughput'; import { getServiceEntitySummary } from '../entities/services/get_service_entity_summary'; import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; -import { createEntitiesESClient } from '../../lib/helpers/create_es_client/create_entities_es_client/create_entities_es_client'; const servicesRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/services', @@ -297,16 +296,11 @@ const serviceAgentRoute = createApmServerRoute({ }), security: { authz: { requiredPrivileges: ['apm'] } }, handler: async (resources): Promise => { - const { context, request } = resources; - const coreContext = await context.core; + const { request, plugins } = resources; + const entityManagerStart = await plugins.entityManager.start(); - const [apmEventClient, entitiesESClient] = await Promise.all([ - getApmEventClient(resources), - createEntitiesESClient({ - request, - esClient: coreContext.elasticsearch.client.asCurrentUser, - }), - ]); + const apmEventClient = await getApmEventClient(resources); + const entityManagerClient = await entityManagerStart.getScopedClient({ request }); const { params } = resources; const { serviceName } = params.path; const { start, end } = params.query; @@ -320,7 +314,7 @@ const serviceAgentRoute = createApmServerRoute({ }), getServiceEntitySummary({ serviceName, - entitiesESClient, + entityManagerClient, environment: ENVIRONMENT_ALL.value, }), ]); diff --git a/x-pack/plugins/observability_solution/apm/server/types.ts b/x-pack/plugins/observability_solution/apm/server/types.ts index e99860b9d441f..ba1d17d6af6b9 100644 --- a/x-pack/plugins/observability_solution/apm/server/types.ts +++ b/x-pack/plugins/observability_solution/apm/server/types.ts @@ -5,52 +5,50 @@ * 2.0. */ -import { SharePluginSetup } from '@kbn/share-plugin/server'; -import { Observable } from 'rxjs'; -import { - RuleRegistryPluginSetupContract, - RuleRegistryPluginStartContract, -} from '@kbn/rule-registry-plugin/server'; -import { - PluginSetup as DataPluginSetup, - PluginStart as DataPluginStart, -} from '@kbn/data-plugin/server'; -import { +import type { ApmDataAccessPluginSetup, ApmDataAccessPluginStart, } from '@kbn/apm-data-access-plugin/server'; - -import { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server'; +import type { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, +} from '@kbn/data-plugin/server'; +import type { + RuleRegistryPluginSetupContract, + RuleRegistryPluginStartContract, +} from '@kbn/rule-registry-plugin/server'; +import type { SharePluginSetup } from '@kbn/share-plugin/server'; +import type { Observable } from 'rxjs'; +import type { ActionsPlugin } from '@kbn/actions-plugin/server'; +import type { CloudSetup } from '@kbn/cloud-plugin/server'; +import type { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; +import type { FeaturesPluginSetup, FeaturesPluginStart } from '@kbn/features-plugin/server'; import { HomeServerPluginSetup, HomeServerPluginStart } from '@kbn/home-plugin/server'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; -import { ActionsPlugin } from '@kbn/actions-plugin/server'; import type { AlertingServerSetup, AlertingServerStart } from '@kbn/alerting-plugin/server'; -import { CloudSetup } from '@kbn/cloud-plugin/server'; -import { FeaturesPluginSetup, FeaturesPluginStart } from '@kbn/features-plugin/server'; import { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugin/server'; import { MlPluginSetup, MlPluginStart } from '@kbn/ml-plugin/server'; import { ObservabilityPluginSetup } from '@kbn/observability-plugin/server'; import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; -import { - TaskManagerSetupContract, - TaskManagerStartContract, -} from '@kbn/task-manager-plugin/server'; import { FleetSetupContract as FleetPluginSetup, FleetStartContract as FleetPluginStart, } from '@kbn/fleet-plugin/server'; -import { MetricsDataPluginSetup } from '@kbn/metrics-data-access-plugin/server'; -import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; - -import { +import type { MetricsDataPluginSetup } from '@kbn/metrics-data-access-plugin/server'; +import type { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server'; +import type { + TaskManagerSetupContract, + TaskManagerStartContract, +} from '@kbn/task-manager-plugin/server'; +import type { CustomIntegrationsPluginSetup, CustomIntegrationsPluginStart, } from '@kbn/custom-integrations-plugin/server'; -import { - ProfilingDataAccessPluginSetup, - ProfilingDataAccessPluginStart, -} from '@kbn/profiling-data-access-plugin/server'; -import { +import type { + EntityManagerServerPluginSetup, + EntityManagerServerPluginStart, +} from '@kbn/entityManager-plugin/server'; +import type { LogsDataAccessPluginSetup, LogsDataAccessPluginStart, } from '@kbn/logs-data-access-plugin/server'; @@ -58,6 +56,10 @@ import type { ObservabilityAIAssistantServerSetup, ObservabilityAIAssistantServerStart, } from '@kbn/observability-ai-assistant-plugin/server'; +import type { + ProfilingDataAccessPluginSetup, + ProfilingDataAccessPluginStart, +} from '@kbn/profiling-data-access-plugin/server'; import { APMConfig } from '.'; export interface APMPluginSetup { @@ -75,8 +77,10 @@ export interface APMPluginSetupDependencies { metricsDataAccess: MetricsDataPluginSetup; dataViews: {}; share: SharePluginSetup; - observabilityAIAssistant?: ObservabilityAIAssistantServerSetup; + logsDataAccess: LogsDataAccessPluginSetup; + entityManager: EntityManagerServerPluginSetup; // optional dependencies + observabilityAIAssistant?: ObservabilityAIAssistantServerSetup; actions?: ActionsPlugin['setup']; alerting?: AlertingServerSetup; cloud?: CloudSetup; @@ -89,7 +93,6 @@ export interface APMPluginSetupDependencies { usageCollection?: UsageCollectionSetup; customIntegrations?: CustomIntegrationsPluginSetup; profilingDataAccess?: ProfilingDataAccessPluginSetup; - logsDataAccess: LogsDataAccessPluginSetup; } export interface APMPluginStartDependencies { // required dependencies @@ -102,8 +105,10 @@ export interface APMPluginStartDependencies { metricsDataAccess: MetricsDataPluginSetup; dataViews: DataViewsServerPluginStart; share: undefined; - observabilityAIAssistant?: ObservabilityAIAssistantServerStart; + logsDataAccess: LogsDataAccessPluginStart; + entityManager: EntityManagerServerPluginStart; // optional dependencies + observabilityAIAssistant?: ObservabilityAIAssistantServerStart; actions?: ActionsPlugin['start']; alerting?: AlertingServerStart; cloud?: undefined; @@ -116,5 +121,4 @@ export interface APMPluginStartDependencies { usageCollection?: undefined; customIntegrations?: CustomIntegrationsPluginStart; profilingDataAccess?: ProfilingDataAccessPluginStart; - logsDataAccess: LogsDataAccessPluginStart; } diff --git a/x-pack/plugins/observability_solution/apm/tsconfig.json b/x-pack/plugins/observability_solution/apm/tsconfig.json index b2fda13c3f76f..82dd827086033 100644 --- a/x-pack/plugins/observability_solution/apm/tsconfig.json +++ b/x-pack/plugins/observability_solution/apm/tsconfig.json @@ -129,6 +129,7 @@ "@kbn/alerting-comparators", "@kbn/saved-search-component", "@kbn/saved-search-plugin", + "@kbn/entityManager-plugin", ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/observability_solution/infra/public/plugin.ts b/x-pack/plugins/observability_solution/infra/public/plugin.ts index c8217794acf70..524ca1841be9b 100644 --- a/x-pack/plugins/observability_solution/infra/public/plugin.ts +++ b/x-pack/plugins/observability_solution/infra/public/plugin.ts @@ -19,7 +19,6 @@ import { i18n } from '@kbn/i18n'; import { METRICS_EXPLORER_LOCATOR_ID, MetricsExplorerLocatorParams, - ObservabilityTriggerId, } from '@kbn/observability-shared-plugin/common'; import { BehaviorSubject, @@ -101,10 +100,6 @@ export class Plugin implements InfraClientPluginClass { registerFeatures(pluginsSetup.home); } - pluginsSetup.uiActions.registerTrigger({ - id: ObservabilityTriggerId.LogEntryContextMenu, - }); - const assetDetailsLocator = pluginsSetup.share.url.locators.get(ASSET_DETAILS_LOCATOR_ID); const inventoryLocator = diff --git a/x-pack/plugins/observability_solution/logs_data_access/kibana.jsonc b/x-pack/plugins/observability_solution/logs_data_access/kibana.jsonc index 02fcff85404a0..f3eccc1c0c154 100644 --- a/x-pack/plugins/observability_solution/logs_data_access/kibana.jsonc +++ b/x-pack/plugins/observability_solution/logs_data_access/kibana.jsonc @@ -1,13 +1,17 @@ { "type": "plugin", "id": "@kbn/logs-data-access-plugin", - "owner": ["@elastic/obs-ux-logs-team"], + "owner": [ + "@elastic/obs-ux-logs-team" + ], + "group": "platform", + "visibility": "shared", "plugin": { "id": "logsDataAccess", "server": true, "browser": true, "requiredPlugins": [ - "data", + "data", "dataViews" ], "optionalPlugins": [], diff --git a/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc b/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc index e6ee1a22edc05..46aa8f451260d 100644 --- a/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc +++ b/x-pack/plugins/observability_solution/logs_shared/kibana.jsonc @@ -2,12 +2,17 @@ "type": "plugin", "id": "@kbn/logs-shared-plugin", "owner": "@elastic/obs-ux-logs-team", + "group": "platform", + "visibility": "shared", "description": "Exposes the shared components and APIs to access and visualize logs.", "plugin": { "id": "logsShared", "server": true, "browser": true, - "configPath": ["xpack", "logs_shared"], + "configPath": [ + "xpack", + "logs_shared" + ], "requiredPlugins": [ "charts", "data", @@ -15,16 +20,21 @@ "dataViews", "discoverShared", "logsDataAccess", - "observabilityShared", "share", "spaces", + "uiActions", "usageCollection", "embeddable", ], "optionalPlugins": [ "observabilityAIAssistant", ], - "requiredBundles": ["kibanaUtils", "kibanaReact"], - "extraPublicDirs": ["common"] + "requiredBundles": [ + "kibanaUtils", + "kibanaReact" + ], + "extraPublicDirs": [ + "common" + ] } } diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/log_stream/log_stream.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/log_stream/log_stream.tsx index f82e9436fe5cd..8d33fe4b5d343 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/components/log_stream/log_stream.tsx +++ b/x-pack/plugins/observability_solution/logs_shared/public/components/log_stream/log_stream.tsx @@ -10,19 +10,19 @@ import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { buildEsQuery, Filter, Query } from '@kbn/es-query'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; import { JsonValue } from '@kbn/utility-types'; import { noop } from 'lodash'; import React, { useCallback, useEffect, useMemo } from 'react'; import usePrevious from 'react-use/lib/usePrevious'; -import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public'; -import { useKibanaQuerySettings } from '@kbn/observability-shared-plugin/public'; import { LogEntryCursor } from '../../../common/log_entry'; import { defaultLogViewsStaticConfig, LogViewReference } from '../../../common/log_views'; import { BuiltEsQuery, useLogStream } from '../../containers/logs/log_stream'; import { useLogView } from '../../hooks/use_log_view'; import { LogViewsClient } from '../../services/log_views'; import { LogColumnRenderConfiguration } from '../../utils/log_column_render_configuration'; +import { useKibanaQuerySettings } from '../../utils/use_kibana_query_settings'; import { useLogEntryFlyout } from '../logging/log_entry_flyout'; import { ScrollableLogTextStreamView, VisibleInterval } from '../logging/log_text_stream'; import { LogStreamErrorBoundary } from './log_stream_error_boundary'; diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_entry_flyout/log_entry_actions_menu.test.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_entry_flyout/log_entry_actions_menu.test.tsx index ad989f8460016..05906b2794c9f 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_entry_flyout/log_entry_actions_menu.test.tsx +++ b/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_entry_flyout/log_entry_actions_menu.test.tsx @@ -7,6 +7,8 @@ import { coreMock } from '@kbn/core/public/mocks'; import { + TRANSACTION_DETAILS_BY_TRACE_ID_LOCATOR, + TransactionDetailsByTraceIdLocatorParams, uptimeOverviewLocatorID, UptimeOverviewLocatorInfraParams, UptimeOverviewLocatorParams, @@ -26,10 +28,10 @@ coreStartMock.application.getUrlForApp.mockImplementation((app, options) => { }); const emptyUrlService = new MockUrlService(); -const urlServiceWithUptimeLocator = new MockUrlService(); +const urlServiceWithMockLocators = new MockUrlService(); // we can't use the actual locator here because its import would create a // forbidden ts project reference cycle -urlServiceWithUptimeLocator.locators.create< +urlServiceWithMockLocators.locators.create< UptimeOverviewLocatorInfraParams | UptimeOverviewLocatorParams >({ id: uptimeOverviewLocatorID, @@ -37,6 +39,12 @@ urlServiceWithUptimeLocator.locators.create< return { app: 'uptime', path: '/overview', state: {} }; }, }); +urlServiceWithMockLocators.locators.create({ + id: TRANSACTION_DETAILS_BY_TRACE_ID_LOCATOR, + getLocation: async (params) => { + return { app: 'apm', path: '/trace-id', state: {} }; + }, +}); const ProviderWrapper: FC> = ({ children, @@ -90,7 +98,7 @@ describe('LogEntryActionsMenu component', () => { describe('uptime link with legacy uptime enabled', () => { it('renders as enabled when a host ip is present in the log entry', () => { const elementWrapper = mount( - + { it('renders as enabled when a container id is present in the log entry', () => { const elementWrapper = mount( - + { it('renders as enabled when a pod uid is present in the log entry', () => { const elementWrapper = mount( - + { it('renders as disabled when no supported field is present in the log entry', () => { const elementWrapper = mount( - + { describe('apm link', () => { it('renders with a trace id filter when present in log entry', () => { const elementWrapper = mount( - + { it('renders with a trace id filter and timestamp when present in log entry', () => { const timestamp = '2019-06-27T17:44:08.693Z'; const elementWrapper = mount( - + { it('renders as disabled when no supported field is present in log entry', () => { const elementWrapper = mount( - + { elementWrapper.update(); expect( - elementWrapper.find(`button${testSubject('~apmLogEntryActionsMenuItem')}`).prop('disabled') + elementWrapper + .find(`${testSubject('~apmLogEntryActionsMenuItem')}`) + .first() + .prop('disabled') ).toEqual(true); }); }); diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx index 4f16d34a489ac..404f6c37bffaa 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx +++ b/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx @@ -7,13 +7,14 @@ import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; import { + TRANSACTION_DETAILS_BY_TRACE_ID_LOCATOR, uptimeOverviewLocatorID, + type TransactionDetailsByTraceIdLocatorParams, type UptimeOverviewLocatorInfraParams, } from '@kbn/deeplinks-observability'; import { FormattedMessage } from '@kbn/i18n-react'; -import { LinkDescriptor, useLinkProps } from '@kbn/observability-shared-plugin/public'; import { getRouterLinkProps } from '@kbn/router-utils'; -import { ILocatorClient } from '@kbn/share-plugin/common/url_service'; +import { BrowserUrlService } from '@kbn/share-plugin/public'; import React, { useMemo } from 'react'; import { LogEntry } from '../../../../common/search_strategies/log_entries/log_entry'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; @@ -33,14 +34,11 @@ export const LogEntryActionsMenu = ({ logEntry }: LogEntryActionsMenuProps) => { } = useKibanaContextForPlugin(); const { hide, isVisible, toggle } = useVisibilityState(false); - const apmLinkDescriptor = useMemo(() => getAPMLink(logEntry), [logEntry]); - - const uptimeLinkProps = getUptimeLink({ locators })(logEntry); - - const apmLinkProps = useLinkProps({ - app: 'apm', - ...(apmLinkDescriptor ? apmLinkDescriptor : {}), - }); + const apmLinkProps = useMemo(() => getAPMLink({ locators })(logEntry), [locators, logEntry]); + const uptimeLinkProps = useMemo( + () => getUptimeLink({ locators })(logEntry), + [locators, logEntry] + ); const menuItems = useMemo( () => [ @@ -58,7 +56,7 @@ export const LogEntryActionsMenu = ({ logEntry }: LogEntryActionsMenuProps) => { , { /> , ], - [apmLinkDescriptor, apmLinkProps, uptimeLinkProps] + [apmLinkProps, uptimeLinkProps] ); const hasMenuItems = useMemo(() => menuItems.length > 0, [menuItems]); @@ -101,8 +99,8 @@ export const LogEntryActionsMenu = ({ logEntry }: LogEntryActionsMenuProps) => { }; const getUptimeLink = - ({ locators }: { locators: ILocatorClient }) => - (logEntry: LogEntry): ContextRouterLinkProps | undefined => { + ({ locators }: { locators: BrowserUrlService['locators'] }) => + (logEntry: LogEntry) => { const uptimeLocator = locators.get(uptimeOverviewLocatorID); if (!uptimeLocator) { @@ -135,47 +133,49 @@ const getUptimeLink = }) as ContextRouterLinkProps; }; -const getAPMLink = (logEntry: LogEntry): LinkDescriptor | undefined => { - const traceId = logEntry.fields.find( - ({ field, value }) => typeof value[0] === 'string' && field === 'trace.id' - )?.value?.[0]; - - if (typeof traceId !== 'string') { - return undefined; - } - - const timestampField = logEntry.fields.find(({ field }) => field === '@timestamp'); - const timestamp = timestampField ? timestampField.value[0] : null; - const { rangeFrom, rangeTo } = - typeof timestamp === 'number' - ? (() => { - const from = new Date(timestamp); - const to = new Date(timestamp); - - from.setMinutes(from.getMinutes() - 10); - to.setMinutes(to.getMinutes() + 10); - - return { rangeFrom: from.toISOString(), rangeTo: to.toISOString() }; - })() - : { rangeFrom: 'now-1y', rangeTo: 'now' }; - - return { - app: 'apm', - pathname: getApmTraceUrl({ traceId, rangeFrom, rangeTo }), - }; -}; +const getAPMLink = + ({ locators }: { locators: BrowserUrlService['locators'] }) => + (logEntry: LogEntry) => { + const traceId = logEntry.fields.find( + ({ field, value }) => typeof value[0] === 'string' && field === 'trace.id' + )?.value?.[0]; -function getApmTraceUrl({ - traceId, - rangeFrom, - rangeTo, -}: { - traceId: string; - rangeFrom: string; - rangeTo: string; -}) { - return `/link-to/trace/${traceId}?` + new URLSearchParams({ rangeFrom, rangeTo }).toString(); -} + if (typeof traceId !== 'string') { + return undefined; + } + + const apmLocator = locators.get( + TRANSACTION_DETAILS_BY_TRACE_ID_LOCATOR + ); + + if (!apmLocator) { + return undefined; + } + + const timestampField = logEntry.fields.find(({ field }) => field === '@timestamp'); + const timestamp = timestampField ? timestampField.value[0] : null; + const { rangeFrom, rangeTo } = + typeof timestamp === 'number' || typeof timestamp === 'string' + ? (() => { + const from = new Date(timestamp); + const to = new Date(timestamp); + + from.setMinutes(from.getMinutes() - 10); + to.setMinutes(to.getMinutes() + 10); + + return { rangeFrom: from.toISOString(), rangeTo: to.toISOString() }; + })() + : { rangeFrom: 'now-1y', rangeTo: 'now' }; + + const apmLocatorParams = { traceId, rangeFrom, rangeTo }; + + // Coercing the return value to ContextRouterLinkProps because + // EuiContextMenuItem defines a too broad type for onClick + return getRouterLinkProps({ + href: apmLocator.getRedirectUrl(apmLocatorParams), + onClick: () => apmLocator.navigate(apmLocatorParams), + }) as ContextRouterLinkProps; + }; export interface ContextRouterLinkProps { href: string | undefined; diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_entry_flyout/log_entry_flyout.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_entry_flyout/log_entry_flyout.tsx index 952ee959e4a72..a66cb1790525a 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_entry_flyout/log_entry_flyout.tsx +++ b/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_entry_flyout/log_entry_flyout.tsx @@ -28,7 +28,6 @@ import { useLogEntry } from '../../../containers/logs/log_entry'; import { CenteredEuiFlyoutBody } from '../../centered_flyout_body'; import { DataSearchErrorCallout } from '../../data_search_error_callout'; import { DataSearchProgress } from '../../data_search_progress'; -import LogAIAssistant from '../../log_ai_assistant/log_ai_assistant'; import { LogEntryActionsMenu } from './log_entry_actions_menu'; import { LogEntryFieldsTable } from './log_entry_fields_table'; @@ -42,7 +41,7 @@ export interface LogEntryFlyoutProps { export const useLogEntryFlyout = (logViewReference: LogViewReference) => { const flyoutRef = useRef(); const { - services: { http, data, share, uiSettings, application, observabilityAIAssistant }, + services: { http, data, share, uiSettings, application, logsShared }, overlays: { openFlyout }, } = useKibanaContextForPlugin(); @@ -58,7 +57,7 @@ export const useLogEntryFlyout = (logViewReference: LogViewReference) => { share, uiSettings, application, - observabilityAIAssistant, + logsShared, }); flyoutRef.current = openFlyout( @@ -72,12 +71,12 @@ export const useLogEntryFlyout = (logViewReference: LogViewReference) => { ); }, [ + logsShared, application, closeLogEntryFlyout, data, http, logViewReference, - observabilityAIAssistant, openFlyout, share, uiSettings, @@ -115,7 +114,11 @@ export const LogEntryFlyout = ({ logEntryId, }); - const { observabilityAIAssistant } = useKibanaContextForPlugin().services; + const { + services: { + logsShared: { LogAIAssistant }, + }, + } = useKibanaContextForPlugin(); useEffect(() => { if (logViewReference && logEntryId) { @@ -183,12 +186,9 @@ export const LogEntryFlyout = ({ } > - {observabilityAIAssistant && ( + {LogAIAssistant && ( - + )} diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_text_stream/index.ts b/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_text_stream/index.ts index 6468ea3d94d22..be32a2be6dc58 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_text_stream/index.ts +++ b/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_text_stream/index.ts @@ -5,12 +5,11 @@ * 2.0. */ -export type { LogEntryStreamItem } from './item'; -export type { LogEntryColumnWidths } from './log_entry_column'; - export { LogColumnHeader } from './column_headers'; export { LogColumnHeadersWrapper } from './column_headers_wrapper'; -export { iconColumnId, LogEntryColumn, useColumnWidths } from './log_entry_column'; +export type { LogEntryStreamItem } from './item'; +export { LogEntryColumn, iconColumnId, useColumnWidths } from './log_entry_column'; +export type { LogEntryColumnWidths } from './log_entry_column'; export { LogEntryContextMenu } from './log_entry_context_menu'; export { LogEntryFieldColumn } from './log_entry_field_column'; export { LogEntryMessageColumn } from './log_entry_message_column'; diff --git a/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_text_stream/log_entry_row.tsx b/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_text_stream/log_entry_row.tsx index b73da833032f4..411b5d0731380 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_text_stream/log_entry_row.tsx +++ b/x-pack/plugins/observability_solution/logs_shared/public/components/logging/log_text_stream/log_entry_row.tsx @@ -6,25 +6,19 @@ */ import { i18n } from '@kbn/i18n'; -import { ObservabilityTriggerId } from '@kbn/observability-shared-plugin/common'; -import { - useUiTracker, - getContextMenuItemsFromActions, -} from '@kbn/observability-shared-plugin/public'; import { isEmpty } from 'lodash'; import React, { memo, useCallback, useMemo, useState } from 'react'; -import useAsync from 'react-use/lib/useAsync'; import { LogColumn, LogEntry } from '../../../../common/log_entry'; import { TextScale } from '../../../../common/log_text_scale'; -import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; import { + LogColumnRenderConfiguration, isFieldColumnRenderConfiguration, isMessageColumnRenderConfiguration, isTimestampColumnRenderConfiguration, - LogColumnRenderConfiguration, } from '../../../utils/log_column_render_configuration'; import { isTimestampColumn } from '../../../utils/log_entry'; -import { iconColumnId, LogEntryColumn, LogEntryColumnWidths } from './log_entry_column'; +import { useUiTracker } from '../../../utils/use_ui_tracker'; +import { LogEntryColumn, LogEntryColumnWidths, iconColumnId } from './log_entry_column'; import { LogEntryContextMenu } from './log_entry_context_menu'; import { LogEntryFieldColumn } from './log_entry_field_column'; import { LogEntryMessageColumn } from './log_entry_message_column'; @@ -74,7 +68,7 @@ export const LogEntryRow = memo( scale, wrap, }: LogEntryRowProps) => { - const trackMetric = useUiTracker({ app: 'infra_logs' }); + const trackMetric = useUiTracker(); const [isHovered, setIsHovered] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -99,16 +93,6 @@ export const LogEntryRow = memo( const hasActionViewLogInContext = hasContext && openViewLogInContext !== undefined; const hasActionsMenu = hasActionFlyoutWithItem || hasActionViewLogInContext; - const uiActions = useKibanaContextForPlugin().services.uiActions; - - const externalContextMenuItems = useAsync(() => { - return getContextMenuItemsFromActions({ - uiActions, - triggerId: ObservabilityTriggerId.LogEntryContextMenu, - context: logEntry, - }); - }, [uiActions, logEntry]); - const menuItems = useMemo(() => { const items = []; if (hasActionFlyoutWithItem) { @@ -251,7 +235,6 @@ export const LogEntryRow = memo( onOpen={openMenu} onClose={closeMenu} items={menuItems} - externalItems={externalContextMenuItems.value} /> ) : null} diff --git a/x-pack/plugins/observability_solution/logs_shared/public/hooks/use_kibana.tsx b/x-pack/plugins/observability_solution/logs_shared/public/hooks/use_kibana.tsx index 09032b4b644a2..af55accdd66b5 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/hooks/use_kibana.tsx +++ b/x-pack/plugins/observability_solution/logs_shared/public/hooks/use_kibana.tsx @@ -20,8 +20,7 @@ import { } from '../types'; export type PluginKibanaContextValue = CoreStart & - LogsSharedClientStartDeps & - LogsSharedClientStartExports; + LogsSharedClientStartDeps & { logsShared: LogsSharedClientStartExports }; export const createKibanaContextForPlugin = ( core: CoreStart, @@ -31,7 +30,7 @@ export const createKibanaContextForPlugin = ( createKibanaReactContext({ ...core, ...plugins, - ...pluginStart, + logsShared: pluginStart, }); export const useKibanaContextForPlugin = diff --git a/x-pack/plugins/observability_solution/logs_shared/public/utils/use_kibana_query_settings.ts b/x-pack/plugins/observability_solution/logs_shared/public/utils/use_kibana_query_settings.ts new file mode 100644 index 0000000000000..521cd0142303b --- /dev/null +++ b/x-pack/plugins/observability_solution/logs_shared/public/utils/use_kibana_query_settings.ts @@ -0,0 +1,31 @@ +/* + * 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 type { EsQueryConfig } from '@kbn/es-query'; +import { SerializableRecord } from '@kbn/utility-types'; +import { useMemo } from 'react'; +import { UI_SETTINGS } from '@kbn/data-plugin/public'; +import { useUiSetting$ } from '@kbn/kibana-react-plugin/public'; + +export const useKibanaQuerySettings = (): EsQueryConfig => { + const [allowLeadingWildcards] = useUiSetting$(UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS); + const [queryStringOptions] = useUiSetting$(UI_SETTINGS.QUERY_STRING_OPTIONS); + const [dateFormatTZ] = useUiSetting$(UI_SETTINGS.DATEFORMAT_TZ); + const [ignoreFilterIfFieldNotInIndex] = useUiSetting$( + UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX + ); + + return useMemo( + () => ({ + allowLeadingWildcards, + queryStringOptions, + dateFormatTZ, + ignoreFilterIfFieldNotInIndex, + }), + [allowLeadingWildcards, dateFormatTZ, ignoreFilterIfFieldNotInIndex, queryStringOptions] + ); +}; diff --git a/x-pack/plugins/observability_solution/logs_shared/public/utils/use_ui_tracker.ts b/x-pack/plugins/observability_solution/logs_shared/public/utils/use_ui_tracker.ts new file mode 100644 index 0000000000000..bc7e3696b993e --- /dev/null +++ b/x-pack/plugins/observability_solution/logs_shared/public/utils/use_ui_tracker.ts @@ -0,0 +1,44 @@ +/* + * 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 { useMemo } from 'react'; +import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics'; +import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; + +/** + * Note: The usage_collection plugin will take care of sending this data to the telemetry server. + * You can find the metrics that are collected by these hooks in Stack Telemetry. + * Search the index `kibana-ui-counter`. You can filter for `eventName` and/or `appName`. + */ + +interface TrackOptions { + metricType?: UiCounterMetricType; + delay?: number; // in ms +} + +interface ServiceDeps { + usageCollection: UsageCollectionSetup; // TODO: This should really be start. Looking into it. +} + +export type TrackMetricOptions = TrackOptions & { metric: string }; +export type UiTracker = ReturnType; +export type TrackEvent = (options: TrackMetricOptions) => void; + +export { METRIC_TYPE }; + +export function useUiTracker(): TrackEvent { + const reportUiCounter = useKibana().services?.usageCollection?.reportUiCounter; + const trackEvent = useMemo(() => { + return ({ metric, metricType = METRIC_TYPE.COUNT }: TrackMetricOptions) => { + if (reportUiCounter) { + reportUiCounter('infra_logs', metricType, metric); + } + }; + }, [reportUiCounter]); + return trackEvent; +} diff --git a/x-pack/plugins/observability_solution/logs_shared/tsconfig.json b/x-pack/plugins/observability_solution/logs_shared/tsconfig.json index cf1bb42b058be..1892e6b4e2dca 100644 --- a/x-pack/plugins/observability_solution/logs_shared/tsconfig.json +++ b/x-pack/plugins/observability_solution/logs_shared/tsconfig.json @@ -11,7 +11,9 @@ "types/**/*", "emotion.d.ts" ], - "exclude": ["target/**/*"], + "exclude": [ + "target/**/*" + ], "kbn_references": [ "@kbn/core", "@kbn/i18n", @@ -29,7 +31,6 @@ "@kbn/logging-mocks", "@kbn/kibana-react-plugin", "@kbn/test-subj-selector", - "@kbn/observability-shared-plugin", "@kbn/datemath", "@kbn/core-http-browser", "@kbn/ui-actions-plugin", @@ -53,5 +54,7 @@ "@kbn/embeddable-plugin", "@kbn/saved-search-plugin", "@kbn/spaces-plugin", + "@kbn/analytics", + "@kbn/usage-collection-plugin", ] } diff --git a/x-pack/plugins/observability_solution/observability_shared/common/entity/entity_types.ts b/x-pack/plugins/observability_solution/observability_shared/common/entity/entity_types.ts index db3e91fbf493a..222780a1fc31a 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/entity/entity_types.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/entity/entity_types.ts @@ -14,6 +14,7 @@ export const BUILT_IN_ENTITY_TYPES = { HOST: 'host', CONTAINER: 'container', SERVICE: 'service', + SERVICE_V2: 'built_in_services_from_ecs_data', KUBERNETES: { CLUSTER: createKubernetesEntity('cluster'), CONTAINER: createKubernetesEntity('container'), diff --git a/x-pack/plugins/observability_solution/observability_shared/common/field_names/elasticsearch.ts b/x-pack/plugins/observability_solution/observability_shared/common/field_names/elasticsearch.ts index e703cd487259c..5569dac69b8be 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/field_names/elasticsearch.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/field_names/elasticsearch.ts @@ -146,6 +146,8 @@ export const PROFILE_ALLOC_SPACE = 'profile.alloc_space.bytes'; export const PROFILE_INUSE_OBJECTS = 'profile.inuse_objects.count'; export const PROFILE_INUSE_SPACE = 'profile.inuse_space.bytes'; +export const DATA_STREAM_TYPE = 'data_stream.type'; + export const ENTITY = 'entity'; export const ENTITY_ID = 'entity.id'; export const ENTITY_TYPE = 'entity.type'; diff --git a/x-pack/plugins/observability_solution/observability_shared/common/index.ts b/x-pack/plugins/observability_solution/observability_shared/common/index.ts index a8e26366ab4b3..24d12362d7cfa 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/index.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/index.ts @@ -128,15 +128,16 @@ export { PROFILE_ALLOC_SPACE, PROFILE_INUSE_OBJECTS, PROFILE_INUSE_SPACE, + DATA_STREAM_TYPE, ENTITY, - ENTITY_DEFINITION_ID, - ENTITY_DISPLAY_NAME, - ENTITY_FIRST_SEEN, ENTITY_ID, - ENTITY_LAST_SEEN, ENTITY_TYPE, - SOURCE_DATA_STREAM_TYPE, + ENTITY_LAST_SEEN, + ENTITY_FIRST_SEEN, + ENTITY_DISPLAY_NAME, + ENTITY_DEFINITION_ID, ENTITY_IDENTITY_FIELDS, + SOURCE_DATA_STREAM_TYPE, } from './field_names/elasticsearch'; export { diff --git a/x-pack/plugins/observability_solution/observability_shared/common/locators/apm/transaction_details_by_trace_id_locator.ts b/x-pack/plugins/observability_solution/observability_shared/common/locators/apm/transaction_details_by_trace_id_locator.ts index 2e461bc4f9d55..94fa1176c3ee0 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/locators/apm/transaction_details_by_trace_id_locator.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/locators/apm/transaction_details_by_trace_id_locator.ts @@ -4,14 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import qs from 'query-string'; import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'; -import type { SerializableRecord } from '@kbn/utility-types'; +import { + TRANSACTION_DETAILS_BY_TRACE_ID_LOCATOR, + type TransactionDetailsByTraceIdLocatorParams, +} from '@kbn/deeplinks-observability'; -export const TRANSACTION_DETAILS_BY_TRACE_ID_LOCATOR = 'TRANSACTION_DETAILS_BY_TRACE_ID_LOCATOR'; - -export interface TransactionDetailsByTraceIdLocatorParams extends SerializableRecord { - traceId: string; -} +export { TRANSACTION_DETAILS_BY_TRACE_ID_LOCATOR, type TransactionDetailsByTraceIdLocatorParams }; export type TransactionDetailsByTraceIdLocator = LocatorPublic; @@ -21,10 +21,15 @@ export class TransactionDetailsByTraceIdLocatorDefinition { public readonly id = TRANSACTION_DETAILS_BY_TRACE_ID_LOCATOR; - public readonly getLocation = async ({ traceId }: TransactionDetailsByTraceIdLocatorParams) => { + public readonly getLocation = async ({ + rangeFrom, + rangeTo, + traceId, + }: TransactionDetailsByTraceIdLocatorParams) => { + const params = { rangeFrom, rangeTo }; return { app: 'apm', - path: `/link-to/trace/${encodeURIComponent(traceId)}`, + path: `/link-to/trace/${encodeURIComponent(traceId)}?${qs.stringify(params)}`, state: {}, }; }; diff --git a/x-pack/plugins/observability_solution/observability_shared/common/trigger_ids.ts b/x-pack/plugins/observability_solution/observability_shared/common/trigger_ids.ts index 404aaab8781b1..8a75472e0546b 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/trigger_ids.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/trigger_ids.ts @@ -6,7 +6,6 @@ */ export enum ObservabilityTriggerId { - LogEntryContextMenu = 'logEntryContextMenu', ApmTransactionContextMenu = 'apmTransactionContextMenu', ApmErrorContextMenu = 'apmErrorContextMenu', } diff --git a/x-pack/plugins/observability_solution/observability_shared/tsconfig.json b/x-pack/plugins/observability_solution/observability_shared/tsconfig.json index f7b8a7ff6c573..15ae8d34c7f55 100644 --- a/x-pack/plugins/observability_solution/observability_shared/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_shared/tsconfig.json @@ -46,6 +46,7 @@ "@kbn/es-query", "@kbn/serverless", "@kbn/data-views-plugin", + "@kbn/deeplinks-observability", ], "exclude": ["target/**/*", ".storybook/**/*.js"] } diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx index 547ee0235e773..1c245dc779d01 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/risk_score_enable_section.tsx @@ -289,7 +289,7 @@ export const RiskScoreEnableSection: React.FC<{ )} diff --git a/x-pack/test/upgrade_assistant_integration/config.ts b/x-pack/test/upgrade_assistant_integration/config.ts index 0794f4d0b9ada..df798a701b6b4 100644 --- a/x-pack/test/upgrade_assistant_integration/config.ts +++ b/x-pack/test/upgrade_assistant_integration/config.ts @@ -38,8 +38,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { }, esTestCluster: { ...xPackFunctionalTestsConfig.get('esTestCluster'), - // this archive can not be loaded into 8.0+ - // dataArchive: path.resolve(__dirname, './fixtures/data_archives/upgrade_assistant.zip'), + dataArchive: path.resolve(__dirname, './fixtures/data_archives/upgrade_assistant.zip'), }, }; } diff --git a/x-pack/test/upgrade_assistant_integration/fixtures/data_archives/upgrade_assistant.zip b/x-pack/test/upgrade_assistant_integration/fixtures/data_archives/upgrade_assistant.zip index bf2104fc59953..bc0208152dae4 100644 Binary files a/x-pack/test/upgrade_assistant_integration/fixtures/data_archives/upgrade_assistant.zip and b/x-pack/test/upgrade_assistant_integration/fixtures/data_archives/upgrade_assistant.zip differ diff --git a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.ts similarity index 85% rename from x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js rename to x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.ts index b8f689edb6a31..f78ac7cb2521d 100644 --- a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.js +++ b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/reindexing.ts @@ -7,18 +7,22 @@ import expect from '@kbn/expect'; -import { ReindexStatus, REINDEX_OP_TYPE } from '@kbn/upgrade-assistant-plugin/common/types'; +import { + ReindexStatus, + REINDEX_OP_TYPE, + type ResolveIndexResponseFromES, +} from '@kbn/upgrade-assistant-plugin/common/types'; import { generateNewIndexName } from '@kbn/upgrade-assistant-plugin/server/lib/reindexing/index_settings'; import { getIndexState } from '@kbn/upgrade-assistant-plugin/common/get_index_state'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; -export default function ({ getService }) { +export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const es = getService('es'); // Utility function that keeps polling API until reindex operation has completed or failed. - const waitForReindexToComplete = async (indexName) => { - console.log(`Waiting for reindex to complete...`); + const waitForReindexToComplete = async (indexName: string) => { let lastState; while (true) { @@ -34,7 +38,7 @@ export default function ({ getService }) { return lastState; }; - describe.skip('reindexing', () => { + describe('reindexing', () => { afterEach(() => { // Cleanup saved objects return es.deleteByQuery({ @@ -71,9 +75,9 @@ export default function ({ getService }) { // The new index was created expect(indexSummary[newIndexName]).to.be.an('object'); // The original index name is aliased to the new one - expect(indexSummary[newIndexName].aliases.dummydata).to.be.an('object'); + expect(indexSummary[newIndexName].aliases?.dummydata).to.be.an('object'); // Verify mappings exist on new index - expect(indexSummary[newIndexName].mappings.properties).to.be.an('object'); + expect(indexSummary[newIndexName].mappings?.properties).to.be.an('object'); // The number of documents in the new index matches what we expect expect((await es.count({ index: lastState.newIndexName })).count).to.be(3); @@ -89,7 +93,7 @@ export default function ({ getService }) { // This new index is the new soon to be created reindexed index. We create it // upfront to simulate a situation in which the user restarted kibana half // way through the reindex process and ended up with an extra index. - await es.indices.create({ index: 'reindexed-v7-dummydata' }); + await es.indices.create({ index: 'reindexed-v9-dummydata' }); const { body } = await supertest .post(`/api/upgrade_assistant/reindex/dummydata`) @@ -148,8 +152,8 @@ export default function ({ getService }) { }); it('shows no warnings', async () => { - const resp = await supertest.get(`/api/upgrade_assistant/reindex/7.0-data`); - // By default all reindexing operations will replace an index with an alias (with the same name) + const resp = await supertest.get(`/api/upgrade_assistant/reindex/reindexed-v8-6.0-data`); // reusing the index previously migrated in v8->v9 UA tests + // By default, all reindexing operations will replace an index with an alias (with the same name) // pointing to a newly created "reindexed" index. expect(resp.body.warnings.length).to.be(1); expect(resp.body.warnings[0].warningType).to.be('replaceIndexWithAlias'); @@ -157,20 +161,23 @@ export default function ({ getService }) { it('reindexes old 7.0 index', async () => { const { body } = await supertest - .post(`/api/upgrade_assistant/reindex/7.0-data`) + .post(`/api/upgrade_assistant/reindex/reindexed-v8-6.0-data`) // reusing the index previously migrated in v8->v9 UA tests .set('kbn-xsrf', 'xxx') .expect(200); - expect(body.indexName).to.equal('7.0-data'); + expect(body.indexName).to.equal('reindexed-v8-6.0-data'); expect(body.status).to.equal(ReindexStatus.inProgress); - const lastState = await waitForReindexToComplete('7.0-data'); + const lastState = await waitForReindexToComplete('reindexed-v8-6.0-data'); expect(lastState.errorMessage).to.equal(null); expect(lastState.status).to.equal(ReindexStatus.completed); }); it('should reindex a batch in order and report queue state', async () => { - const assertQueueState = async (firstInQueueIndexName, queueLength) => { + const assertQueueState = async ( + firstInQueueIndexName: string | undefined, + queueLength: number + ) => { const response = await supertest .get(`/api/upgrade_assistant/reindex/batch/queue`) .set('kbn-xsrf', 'xxx') @@ -193,13 +200,13 @@ export default function ({ getService }) { const test2 = 'batch-reindex-test2'; const test3 = 'batch-reindex-test3'; - const cleanupReindex = async (indexName) => { + const cleanupReindex = async (indexName: string) => { try { await es.indices.delete({ index: generateNewIndexName(indexName) }); } catch (e) { try { await es.indices.delete({ index: indexName }); - } catch (e) { + } catch (_err) { // Ignore } } @@ -240,7 +247,10 @@ export default function ({ getService }) { name: nameOfIndexThatShouldBeClosed, }); - const test1ReindexedState = getIndexState(nameOfIndexThatShouldBeClosed, resolvedIndices); + const test1ReindexedState = getIndexState( + nameOfIndexThatShouldBeClosed, + resolvedIndices as ResolveIndexResponseFromES + ); expect(test1ReindexedState).to.be('closed'); } finally { await cleanupReindex(test1);