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);