[Inventory v2] Fix issue with logs only views (#207305)

Closes https://github.com/elastic/kibana/issues/206967

## Summary

After some changes related to V2 migration of getting the entities,
there was an issue with the new data coming from the endpoint - the
`data_stream.type` is a string instead of an array in case of a single
data stream so this PR adds a fix to support that (and a test)

## Bug fixes
- Service overview page loads for a logs-only data stream
- After adding the fix, I saw another error related to the `useTheme`
and changed it to use the `euiTheme` similar to the other changes
related to the Borealis team upgrade

## Testing
To test the scenario with services and hosts coming from logs (without
APM / metrics) I added a new scenario in synthtrace so to test then we
should:
- Run the new scenario: `node scripts/synthtrace logs_only` (if possible
on a clean ES)
- Enable `observability:entityCentricExperience` in Stack Management >
Advanced Setting
- Go to Inventory and click on a service
   - The logs-only views should be available
- Go to Inventory and click on a host
   - The logs-only views should be available
   


https://github.com/user-attachments/assets/cfd5fd40-ac44-4807-9a29-f3ee3015d814


 - Test one of the scenarios with mix of APM/metrics/logs
    - Run `node scripts/synthtrace infra_hosts_with_apm_hosts`
- Enable `observability:entityCentricExperience` in Stack Management >
Advanced Setting
    - Go to Inventory and click on a service from APM
       - The APM views (service/traces) should be available
    - Go to Inventory and click on a host
       - The asset details view should be available and show metrics
     


https://github.com/user-attachments/assets/894c7c1a-aaa1-42cb-9dcb-05c9a5ca8177



- Infrastructure (Inventory/Hosts, etc) and Applications (Service
Inventory/Traces, etc) should load the data for this scenario and not
for the logs only (also for an oblt cluster connection)
    
    


https://github.com/user-attachments/assets/4d092cc6-a8ad-4022-b980-b443be09acc9
This commit is contained in:
jennypavlova 2025-01-21 17:29:05 +01:00 committed by GitHub
parent ff85b0fbff
commit bd5e8ca320
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 113 additions and 9 deletions

View file

@ -0,0 +1,79 @@
/*
* 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 { LogDocument, log, generateShortId, generateLongId } from '@kbn/apm-synthtrace-client';
import { Scenario } from '../cli/scenario';
import { withClient } from '../lib/utils/with_client';
import { parseLogsScenarioOpts } from './helpers/logs_scenario_opts_parser';
import { IndexTemplateName } from '../lib/logs/custom_logsdb_index_templates';
import { getCluster, getCloudRegion, getCloudProvider } from './helpers/logs_mock_data';
const MESSAGE_LOG_LEVELS = [
{ message: 'A simple log', level: 'info' },
{ message: 'Yet another debug log', level: 'debug' },
{ message: 'Error with certificate: "ca_trusted_fingerprint"', level: 'error' },
];
const scenario: Scenario<LogDocument> = async (runOptions) => {
const { isLogsDb } = parseLogsScenarioOpts(runOptions.scenarioOpts);
return {
bootstrap: async ({ logsEsClient }) => {
if (isLogsDb) await logsEsClient.createIndexTemplate(IndexTemplateName.LogsDb);
},
generate: ({ range, clients: { logsEsClient } }) => {
const { logger } = runOptions;
const SERVICE_NAMES = Array(3)
.fill(null)
.map((_, idx) => `synth-service-logs-${idx}`);
const logs = range
.interval('1m')
.rate(1)
.generator((timestamp) => {
return Array(20)
.fill(0)
.map(() => {
const index = Math.floor(Math.random() * 3);
const { clusterId, clusterName } = getCluster(index);
const cloudRegion = getCloudRegion(index);
return log
.create({ isLogsDb })
.message(MESSAGE_LOG_LEVELS[index].message)
.logLevel(MESSAGE_LOG_LEVELS[index].level)
.service(SERVICE_NAMES[index])
.defaults({
'trace.id': generateShortId(),
'agent.name': 'synth-agent',
'orchestrator.cluster.name': clusterName,
'orchestrator.cluster.id': clusterId,
'orchestrator.resource.id': generateShortId(),
'cloud.provider': getCloudProvider(),
'cloud.region': cloudRegion,
'cloud.availability_zone': `${cloudRegion}a`,
'cloud.project.id': generateShortId(),
'cloud.instance.id': generateShortId(),
'log.file.path': `/logs/${generateLongId()}/error.txt`,
})
.timestamp(timestamp);
});
});
return [
withClient(
logsEsClient,
logger.perf('generating_logs', () => logs)
),
];
},
};
};
export default scenario;

View file

@ -302,4 +302,27 @@ describe('mergeEntities', () => {
},
]);
});
it('has a single data stream type and no environment', () => {
const entities: EntityLatestServiceRaw[] = [
{
'data_stream.type': 'logs',
'agent.name': ['nodejs', '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 });
expect(result).toEqual([
{
agentName: 'nodejs' as AgentName,
dataStreamTypes: ['logs'],
environments: [],
lastSeenTimestamp: '2024-12-13T14:52:35.461Z',
serviceName: 'service-1',
},
]);
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { compact, uniq } from 'lodash';
import { castArray, compact, uniq } from 'lodash';
import type { EntityDataStreamType } from '@kbn/observability-shared-plugin/common';
import type { EntityLatestServiceRaw } from '../types';
import type { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
@ -49,7 +49,7 @@ function mergeFunc(entity: EntityLatestServiceRaw, existingEntity?: MergedServic
if (!existingEntity) {
return {
...commonEntityFields,
dataStreamTypes: uniq(entity['data_stream.type']),
dataStreamTypes: uniq(castArray(entity['data_stream.type'])),
environments: uniq(
compact(
Array.isArray(entity['service.environment'])
@ -62,7 +62,10 @@ function mergeFunc(entity: EntityLatestServiceRaw, existingEntity?: MergedServic
return {
...commonEntityFields,
dataStreamTypes: uniq(
compact([...(existingEntity?.dataStreamTypes ?? []), ...entity['data_stream.type']])
compact([
...(existingEntity?.dataStreamTypes ?? []),
...castArray(entity['data_stream.type']),
])
),
environments: uniq(compact([...existingEntity?.environments, entity['service.environment']])),
};

View file

@ -8,6 +8,7 @@
/* eslint-disable @elastic/eui/href-or-on-click */
import {
COLOR_MODES_STANDARD,
EuiButton,
EuiButtonIcon,
EuiFlexGroup,
@ -22,7 +23,6 @@ import {
} from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { useTheme } from '../../hooks/use_theme';
interface AddDataPanelContent {
title: string;
@ -78,11 +78,10 @@ export function AddDataPanel({
onAddData,
'data-test-subj': dataTestSubj,
}: AddDataPanelProps) {
const { euiTheme } = useEuiTheme();
const theme = useTheme();
const imgSrc = `${content.img?.baseFolderPath}/${theme.darkMode ? 'dark' : 'light'}/${
content.img?.name
}`;
const { euiTheme, colorMode } = useEuiTheme();
const imgSrc = `${content.img?.baseFolderPath}/${
colorMode === COLOR_MODES_STANDARD.dark ? 'dark' : 'light'
}/${content.img?.name}`;
return (
<>