mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Logs Overview] Overview component (iteration 1) (attempt 2) (#195673)
This is a re-submission of https://github.com/elastic/kibana/pull/191899, which was reverted due to a storybook build problem. This introduces a "Logs Overview" component for use in solution UIs behind a feature flag. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kerry Gallagher <471693+Kerry350@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
44a42a7a2a
commit
0caea22006
76 changed files with 3416 additions and 57 deletions
|
@ -978,6 +978,7 @@ module.exports = {
|
|||
files: [
|
||||
'x-pack/plugins/observability_solution/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)',
|
||||
'src/plugins/ai_assistant_management/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)',
|
||||
'x-pack/packages/observability/logs_overview/**/!(*.stories.tsx|*.test.tsx|*.storybook_decorator.tsx|*.mock.tsx)',
|
||||
],
|
||||
rules: {
|
||||
'@kbn/i18n/strings_should_be_translated_with_i18n': 'warn',
|
||||
|
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -652,6 +652,7 @@ x-pack/packages/observability/alerting_test_data @elastic/obs-ux-management-team
|
|||
x-pack/test/cases_api_integration/common/plugins/observability @elastic/response-ops
|
||||
x-pack/packages/observability/get_padded_alert_time_range_util @elastic/obs-ux-management-team
|
||||
x-pack/plugins/observability_solution/observability_logs_explorer @elastic/obs-ux-logs-team
|
||||
x-pack/packages/observability/logs_overview @elastic/obs-ux-logs-team
|
||||
x-pack/plugins/observability_solution/observability_onboarding/e2e @elastic/obs-ux-logs-team @elastic/obs-ux-onboarding-team
|
||||
x-pack/plugins/observability_solution/observability_onboarding @elastic/obs-ux-logs-team @elastic/obs-ux-onboarding-team
|
||||
x-pack/plugins/observability_solution/observability @elastic/obs-ux-management-team
|
||||
|
|
|
@ -97,6 +97,7 @@
|
|||
"@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.cd77847.0",
|
||||
"@types/react": "~18.2.0",
|
||||
"@types/react-dom": "~18.2.0",
|
||||
"@xstate5/react/**/xstate": "^5.18.1",
|
||||
"globby/fast-glob": "^3.2.11"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -687,6 +688,7 @@
|
|||
"@kbn/observability-fixtures-plugin": "link:x-pack/test/cases_api_integration/common/plugins/observability",
|
||||
"@kbn/observability-get-padded-alert-time-range-util": "link:x-pack/packages/observability/get_padded_alert_time_range_util",
|
||||
"@kbn/observability-logs-explorer-plugin": "link:x-pack/plugins/observability_solution/observability_logs_explorer",
|
||||
"@kbn/observability-logs-overview": "link:x-pack/packages/observability/logs_overview",
|
||||
"@kbn/observability-onboarding-plugin": "link:x-pack/plugins/observability_solution/observability_onboarding",
|
||||
"@kbn/observability-plugin": "link:x-pack/plugins/observability_solution/observability",
|
||||
"@kbn/observability-shared-plugin": "link:x-pack/plugins/observability_solution/observability_shared",
|
||||
|
@ -1050,6 +1052,7 @@
|
|||
"@turf/helpers": "6.0.1",
|
||||
"@turf/length": "^6.0.2",
|
||||
"@xstate/react": "^3.2.2",
|
||||
"@xstate5/react": "npm:@xstate/react@^4.1.2",
|
||||
"adm-zip": "^0.5.9",
|
||||
"ai": "^2.2.33",
|
||||
"ajv": "^8.12.0",
|
||||
|
@ -1283,6 +1286,7 @@
|
|||
"whatwg-fetch": "^3.0.0",
|
||||
"xml2js": "^0.5.0",
|
||||
"xstate": "^4.38.2",
|
||||
"xstate5": "npm:xstate@^5.18.1",
|
||||
"xterm": "^5.1.0",
|
||||
"yauzl": "^2.10.0",
|
||||
"yazl": "^2.5.1",
|
||||
|
@ -1304,6 +1308,7 @@
|
|||
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
|
||||
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||
"@babel/plugin-transform-class-properties": "^7.24.7",
|
||||
"@babel/plugin-transform-logical-assignment-operators": "^7.24.7",
|
||||
"@babel/plugin-transform-numeric-separator": "^7.24.7",
|
||||
"@babel/plugin-transform-runtime": "^7.24.7",
|
||||
"@babel/preset-env": "^7.24.7",
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export type ObjectEntry<T> = [keyof T, T[keyof T]];
|
||||
|
||||
export type Fields<TMeta extends Record<string, any> | undefined = undefined> = {
|
||||
'@timestamp'?: number;
|
||||
} & (TMeta extends undefined ? {} : Partial<{ meta: TMeta }>);
|
||||
|
@ -27,4 +29,14 @@ export class Entity<TFields extends Fields> {
|
|||
|
||||
return this;
|
||||
}
|
||||
|
||||
overrides(overrides: Partial<TFields>) {
|
||||
const overrideEntries = Object.entries(overrides) as Array<ObjectEntry<TFields>>;
|
||||
|
||||
overrideEntries.forEach(([fieldName, value]) => {
|
||||
this.fields[fieldName] = value;
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", 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 { castArray } from 'lodash';
|
||||
import { SynthtraceGenerator } from '../types';
|
||||
import { Fields } from './entity';
|
||||
import { Serializable } from './serializable';
|
||||
|
||||
export class GaussianEvents<TFields extends Fields = Fields> {
|
||||
constructor(
|
||||
private readonly from: Date,
|
||||
private readonly to: Date,
|
||||
private readonly mean: Date,
|
||||
private readonly width: number,
|
||||
private readonly totalPoints: number
|
||||
) {}
|
||||
|
||||
*generator<TGeneratedFields extends Fields = TFields>(
|
||||
map: (
|
||||
timestamp: number,
|
||||
index: number
|
||||
) => Serializable<TGeneratedFields> | Array<Serializable<TGeneratedFields>>
|
||||
): SynthtraceGenerator<TGeneratedFields> {
|
||||
if (this.totalPoints <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = this.from.getTime();
|
||||
const endTime = this.to.getTime();
|
||||
const meanTime = this.mean.getTime();
|
||||
const densityInterval = 1 / (this.totalPoints - 1);
|
||||
|
||||
for (let eventIndex = 0; eventIndex < this.totalPoints; eventIndex++) {
|
||||
const quantile = eventIndex * densityInterval;
|
||||
|
||||
const standardScore = Math.sqrt(2) * inverseError(2 * quantile - 1);
|
||||
const timestamp = Math.round(meanTime + standardScore * this.width);
|
||||
|
||||
if (timestamp >= startTime && timestamp <= endTime) {
|
||||
yield* this.generateEvents(timestamp, eventIndex, map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private *generateEvents<TGeneratedFields extends Fields = TFields>(
|
||||
timestamp: number,
|
||||
eventIndex: number,
|
||||
map: (
|
||||
timestamp: number,
|
||||
index: number
|
||||
) => Serializable<TGeneratedFields> | Array<Serializable<TGeneratedFields>>
|
||||
): Generator<Serializable<TGeneratedFields>> {
|
||||
const events = castArray(map(timestamp, eventIndex));
|
||||
for (const event of events) {
|
||||
yield event;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function inverseError(x: number): number {
|
||||
const a = 0.147;
|
||||
const sign = x < 0 ? -1 : 1;
|
||||
|
||||
const part1 = 2 / (Math.PI * a) + Math.log(1 - x * x) / 2;
|
||||
const part2 = Math.log(1 - x * x) / a;
|
||||
|
||||
return sign * Math.sqrt(Math.sqrt(part1 * part1 - part2) - part1);
|
||||
}
|
|
@ -27,7 +27,7 @@ interface HostDocument extends Fields {
|
|||
'cloud.provider'?: string;
|
||||
}
|
||||
|
||||
class Host extends Entity<HostDocument> {
|
||||
export class Host extends Entity<HostDocument> {
|
||||
cpu({ cpuTotalValue }: { cpuTotalValue?: number } = {}) {
|
||||
return new HostMetrics({
|
||||
...this.fields,
|
||||
|
@ -175,3 +175,11 @@ export function host(name: string): Host {
|
|||
'cloud.provider': 'gcp',
|
||||
});
|
||||
}
|
||||
|
||||
export function minimalHost(name: string): Host {
|
||||
return new Host({
|
||||
'agent.id': 'synthtrace',
|
||||
'host.hostname': name,
|
||||
'host.name': name,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import { dockerContainer, DockerContainerMetricsDocument } from './docker_container';
|
||||
import { host, HostMetricsDocument } from './host';
|
||||
import { host, HostMetricsDocument, minimalHost } from './host';
|
||||
import { k8sContainer, K8sContainerMetricsDocument } from './k8s_container';
|
||||
import { pod, PodMetricsDocument } from './pod';
|
||||
import { awsRds, AWSRdsMetricsDocument } from './aws/rds';
|
||||
|
@ -24,6 +24,7 @@ export type InfraDocument =
|
|||
|
||||
export const infra = {
|
||||
host,
|
||||
minimalHost,
|
||||
pod,
|
||||
dockerContainer,
|
||||
k8sContainer,
|
||||
|
|
|
@ -34,6 +34,10 @@ interface IntervalOptions {
|
|||
rate?: number;
|
||||
}
|
||||
|
||||
interface StepDetails {
|
||||
stepMilliseconds: number;
|
||||
}
|
||||
|
||||
export class Interval<TFields extends Fields = Fields> {
|
||||
private readonly intervalAmount: number;
|
||||
private readonly intervalUnit: unitOfTime.DurationConstructor;
|
||||
|
@ -46,12 +50,16 @@ export class Interval<TFields extends Fields = Fields> {
|
|||
this._rate = options.rate || 1;
|
||||
}
|
||||
|
||||
private getIntervalMilliseconds(): number {
|
||||
return moment.duration(this.intervalAmount, this.intervalUnit).asMilliseconds();
|
||||
}
|
||||
|
||||
private getTimestamps() {
|
||||
const from = this.options.from.getTime();
|
||||
const to = this.options.to.getTime();
|
||||
|
||||
let time: number = from;
|
||||
const diff = moment.duration(this.intervalAmount, this.intervalUnit).asMilliseconds();
|
||||
const diff = this.getIntervalMilliseconds();
|
||||
|
||||
const timestamps: number[] = [];
|
||||
|
||||
|
@ -68,15 +76,19 @@ export class Interval<TFields extends Fields = Fields> {
|
|||
*generator<TGeneratedFields extends Fields = TFields>(
|
||||
map: (
|
||||
timestamp: number,
|
||||
index: number
|
||||
index: number,
|
||||
stepDetails: StepDetails
|
||||
) => Serializable<TGeneratedFields> | Array<Serializable<TGeneratedFields>>
|
||||
): SynthtraceGenerator<TGeneratedFields> {
|
||||
const timestamps = this.getTimestamps();
|
||||
const stepDetails: StepDetails = {
|
||||
stepMilliseconds: this.getIntervalMilliseconds(),
|
||||
};
|
||||
|
||||
let index = 0;
|
||||
|
||||
for (const timestamp of timestamps) {
|
||||
const events = castArray(map(timestamp, index));
|
||||
const events = castArray(map(timestamp, index, stepDetails));
|
||||
index++;
|
||||
for (const event of events) {
|
||||
yield event;
|
||||
|
|
|
@ -68,6 +68,7 @@ export type LogDocument = Fields &
|
|||
'event.duration': number;
|
||||
'event.start': Date;
|
||||
'event.end': Date;
|
||||
labels?: Record<string, string>;
|
||||
test_field: string | string[];
|
||||
date: Date;
|
||||
severity: string;
|
||||
|
@ -156,6 +157,26 @@ function create(logsOptions: LogsOptions = defaultLogsOptions): Log {
|
|||
).dataset('synth');
|
||||
}
|
||||
|
||||
function createMinimal({
|
||||
dataset = 'synth',
|
||||
namespace = 'default',
|
||||
}: {
|
||||
dataset?: string;
|
||||
namespace?: string;
|
||||
} = {}): Log {
|
||||
return new Log(
|
||||
{
|
||||
'input.type': 'logs',
|
||||
'data_stream.namespace': namespace,
|
||||
'data_stream.type': 'logs',
|
||||
'data_stream.dataset': dataset,
|
||||
'event.dataset': dataset,
|
||||
},
|
||||
{ isLogsDb: false }
|
||||
);
|
||||
}
|
||||
|
||||
export const log = {
|
||||
create,
|
||||
createMinimal,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { PoissonEvents } from './poisson_events';
|
||||
import { Serializable } from './serializable';
|
||||
|
||||
describe('poisson events', () => {
|
||||
it('generates events within the given time range', () => {
|
||||
const poissonEvents = new PoissonEvents(new Date(1000), new Date(2000), 10);
|
||||
|
||||
const events = Array.from(
|
||||
poissonEvents.generator((timestamp) => new Serializable({ '@timestamp': timestamp }))
|
||||
);
|
||||
|
||||
expect(events.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
for (const event of events) {
|
||||
expect(event.fields['@timestamp']).toBeGreaterThanOrEqual(1000);
|
||||
expect(event.fields['@timestamp']).toBeLessThanOrEqual(2000);
|
||||
}
|
||||
});
|
||||
|
||||
it('generates at least one event if the rate is greater than 0', () => {
|
||||
const poissonEvents = new PoissonEvents(new Date(1000), new Date(2000), 1);
|
||||
|
||||
const events = Array.from(
|
||||
poissonEvents.generator((timestamp) => new Serializable({ '@timestamp': timestamp }))
|
||||
);
|
||||
|
||||
expect(events.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
for (const event of events) {
|
||||
expect(event.fields['@timestamp']).toBeGreaterThanOrEqual(1000);
|
||||
expect(event.fields['@timestamp']).toBeLessThanOrEqual(2000);
|
||||
}
|
||||
});
|
||||
|
||||
it('generates no event if the rate is 0', () => {
|
||||
const poissonEvents = new PoissonEvents(new Date(1000), new Date(2000), 0);
|
||||
|
||||
const events = Array.from(
|
||||
poissonEvents.generator((timestamp) => new Serializable({ '@timestamp': timestamp }))
|
||||
);
|
||||
|
||||
expect(events.length).toBe(0);
|
||||
});
|
||||
});
|
77
packages/kbn-apm-synthtrace-client/src/lib/poisson_events.ts
Normal file
77
packages/kbn-apm-synthtrace-client/src/lib/poisson_events.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 { castArray } from 'lodash';
|
||||
import { SynthtraceGenerator } from '../types';
|
||||
import { Fields } from './entity';
|
||||
import { Serializable } from './serializable';
|
||||
|
||||
export class PoissonEvents<TFields extends Fields = Fields> {
|
||||
constructor(
|
||||
private readonly from: Date,
|
||||
private readonly to: Date,
|
||||
private readonly rate: number
|
||||
) {}
|
||||
|
||||
private getTotalTimePeriod(): number {
|
||||
return this.to.getTime() - this.from.getTime();
|
||||
}
|
||||
|
||||
private getInterarrivalTime(): number {
|
||||
const distribution = -Math.log(1 - Math.random()) / this.rate;
|
||||
const totalTimePeriod = this.getTotalTimePeriod();
|
||||
return Math.floor(distribution * totalTimePeriod);
|
||||
}
|
||||
|
||||
*generator<TGeneratedFields extends Fields = TFields>(
|
||||
map: (
|
||||
timestamp: number,
|
||||
index: number
|
||||
) => Serializable<TGeneratedFields> | Array<Serializable<TGeneratedFields>>
|
||||
): SynthtraceGenerator<TGeneratedFields> {
|
||||
if (this.rate <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let currentTime = this.from.getTime();
|
||||
const endTime = this.to.getTime();
|
||||
let eventIndex = 0;
|
||||
|
||||
while (currentTime < endTime) {
|
||||
const interarrivalTime = this.getInterarrivalTime();
|
||||
currentTime += interarrivalTime;
|
||||
|
||||
if (currentTime < endTime) {
|
||||
yield* this.generateEvents(currentTime, eventIndex, map);
|
||||
eventIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// ensure at least one event has been emitted
|
||||
if (this.rate > 0 && eventIndex === 0) {
|
||||
const forcedEventTime =
|
||||
this.from.getTime() + Math.floor(Math.random() * this.getTotalTimePeriod());
|
||||
yield* this.generateEvents(forcedEventTime, eventIndex, map);
|
||||
}
|
||||
}
|
||||
|
||||
private *generateEvents<TGeneratedFields extends Fields = TFields>(
|
||||
timestamp: number,
|
||||
eventIndex: number,
|
||||
map: (
|
||||
timestamp: number,
|
||||
index: number
|
||||
) => Serializable<TGeneratedFields> | Array<Serializable<TGeneratedFields>>
|
||||
): Generator<Serializable<TGeneratedFields>> {
|
||||
const events = castArray(map(timestamp, eventIndex));
|
||||
for (const event of events) {
|
||||
yield event;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,10 +9,12 @@
|
|||
|
||||
import datemath from '@kbn/datemath';
|
||||
import type { Moment } from 'moment';
|
||||
import { GaussianEvents } from './gaussian_events';
|
||||
import { Interval } from './interval';
|
||||
import { PoissonEvents } from './poisson_events';
|
||||
|
||||
export class Timerange {
|
||||
constructor(private from: Date, private to: Date) {}
|
||||
constructor(public readonly from: Date, public readonly to: Date) {}
|
||||
|
||||
interval(interval: string) {
|
||||
return new Interval({ from: this.from, to: this.to, interval });
|
||||
|
@ -21,6 +23,29 @@ export class Timerange {
|
|||
ratePerMinute(rate: number) {
|
||||
return this.interval(`1m`).rate(rate);
|
||||
}
|
||||
|
||||
poissonEvents(rate: number) {
|
||||
return new PoissonEvents(this.from, this.to, rate);
|
||||
}
|
||||
|
||||
gaussianEvents(mean: Date, width: number, totalPoints: number) {
|
||||
return new GaussianEvents(this.from, this.to, mean, width, totalPoints);
|
||||
}
|
||||
|
||||
splitInto(segmentCount: number): Timerange[] {
|
||||
const duration = this.to.getTime() - this.from.getTime();
|
||||
const segmentDuration = duration / segmentCount;
|
||||
|
||||
return Array.from({ length: segmentCount }, (_, i) => {
|
||||
const from = new Date(this.from.getTime() + i * segmentDuration);
|
||||
const to = new Date(from.getTime() + segmentDuration);
|
||||
return new Timerange(from, to);
|
||||
});
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `Timerange(from=${this.from.toISOString()}, to=${this.to.toISOString()})`;
|
||||
}
|
||||
}
|
||||
|
||||
type DateLike = Date | number | Moment | string;
|
||||
|
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* 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 { infra, LogDocument, log } from '@kbn/apm-synthtrace-client';
|
||||
import { fakerEN as faker } from '@faker-js/faker';
|
||||
import { z } from '@kbn/zod';
|
||||
import { Scenario } from '../cli/scenario';
|
||||
import { withClient } from '../lib/utils/with_client';
|
||||
import {
|
||||
LogMessageGenerator,
|
||||
generateUnstructuredLogMessage,
|
||||
unstructuredLogMessageGenerators,
|
||||
} from './helpers/unstructured_logs';
|
||||
|
||||
const scenarioOptsSchema = z.intersection(
|
||||
z.object({
|
||||
randomSeed: z.number().default(0),
|
||||
messageGroup: z
|
||||
.enum([
|
||||
'httpAccess',
|
||||
'userAuthentication',
|
||||
'networkEvent',
|
||||
'dbOperations',
|
||||
'taskOperations',
|
||||
'degradedOperations',
|
||||
'errorOperations',
|
||||
])
|
||||
.default('dbOperations'),
|
||||
}),
|
||||
z
|
||||
.discriminatedUnion('distribution', [
|
||||
z.object({
|
||||
distribution: z.literal('uniform'),
|
||||
rate: z.number().default(1),
|
||||
}),
|
||||
z.object({
|
||||
distribution: z.literal('poisson'),
|
||||
rate: z.number().default(1),
|
||||
}),
|
||||
z.object({
|
||||
distribution: z.literal('gaussian'),
|
||||
mean: z.coerce.date().describe('Time of the peak of the gaussian distribution'),
|
||||
width: z.number().default(5000).describe('Width of the gaussian distribution in ms'),
|
||||
totalPoints: z
|
||||
.number()
|
||||
.default(100)
|
||||
.describe('Total number of points in the gaussian distribution'),
|
||||
}),
|
||||
])
|
||||
.default({ distribution: 'uniform', rate: 1 })
|
||||
);
|
||||
|
||||
type ScenarioOpts = z.output<typeof scenarioOptsSchema>;
|
||||
|
||||
const scenario: Scenario<LogDocument> = async (runOptions) => {
|
||||
return {
|
||||
generate: ({ range, clients: { logsEsClient } }) => {
|
||||
const { logger } = runOptions;
|
||||
const scenarioOpts = scenarioOptsSchema.parse(runOptions.scenarioOpts ?? {});
|
||||
|
||||
faker.seed(scenarioOpts.randomSeed);
|
||||
faker.setDefaultRefDate(range.from.toISOString());
|
||||
|
||||
logger.debug(`Generating ${scenarioOpts.distribution} logs...`);
|
||||
|
||||
// Logs Data logic
|
||||
const LOG_LEVELS = ['info', 'debug', 'error', 'warn', 'trace', 'fatal'];
|
||||
|
||||
const clusterDefinions = [
|
||||
{
|
||||
'orchestrator.cluster.id': faker.string.nanoid(),
|
||||
'orchestrator.cluster.name': 'synth-cluster-1',
|
||||
'orchestrator.namespace': 'default',
|
||||
'cloud.provider': 'gcp',
|
||||
'cloud.region': 'eu-central-1',
|
||||
'cloud.availability_zone': 'eu-central-1a',
|
||||
'cloud.project.id': faker.string.nanoid(),
|
||||
},
|
||||
{
|
||||
'orchestrator.cluster.id': faker.string.nanoid(),
|
||||
'orchestrator.cluster.name': 'synth-cluster-2',
|
||||
'orchestrator.namespace': 'production',
|
||||
'cloud.provider': 'aws',
|
||||
'cloud.region': 'us-east-1',
|
||||
'cloud.availability_zone': 'us-east-1a',
|
||||
'cloud.project.id': faker.string.nanoid(),
|
||||
},
|
||||
{
|
||||
'orchestrator.cluster.id': faker.string.nanoid(),
|
||||
'orchestrator.cluster.name': 'synth-cluster-3',
|
||||
'orchestrator.namespace': 'kube',
|
||||
'cloud.provider': 'azure',
|
||||
'cloud.region': 'area-51',
|
||||
'cloud.availability_zone': 'area-51a',
|
||||
'cloud.project.id': faker.string.nanoid(),
|
||||
},
|
||||
];
|
||||
|
||||
const hostEntities = [
|
||||
{
|
||||
'host.name': 'host-1',
|
||||
'agent.id': 'synth-agent-1',
|
||||
'agent.name': 'nodejs',
|
||||
'cloud.instance.id': faker.string.nanoid(),
|
||||
'orchestrator.resource.id': faker.string.nanoid(),
|
||||
...clusterDefinions[0],
|
||||
},
|
||||
{
|
||||
'host.name': 'host-2',
|
||||
'agent.id': 'synth-agent-2',
|
||||
'agent.name': 'custom',
|
||||
'cloud.instance.id': faker.string.nanoid(),
|
||||
'orchestrator.resource.id': faker.string.nanoid(),
|
||||
...clusterDefinions[1],
|
||||
},
|
||||
{
|
||||
'host.name': 'host-3',
|
||||
'agent.id': 'synth-agent-3',
|
||||
'agent.name': 'python',
|
||||
'cloud.instance.id': faker.string.nanoid(),
|
||||
'orchestrator.resource.id': faker.string.nanoid(),
|
||||
...clusterDefinions[2],
|
||||
},
|
||||
].map((hostDefinition) =>
|
||||
infra.minimalHost(hostDefinition['host.name']).overrides(hostDefinition)
|
||||
);
|
||||
|
||||
const serviceNames = Array(3)
|
||||
.fill(null)
|
||||
.map((_, idx) => `synth-service-${idx}`);
|
||||
|
||||
const generatorFactory =
|
||||
scenarioOpts.distribution === 'uniform'
|
||||
? range.interval('1s').rate(scenarioOpts.rate)
|
||||
: scenarioOpts.distribution === 'poisson'
|
||||
? range.poissonEvents(scenarioOpts.rate)
|
||||
: range.gaussianEvents(scenarioOpts.mean, scenarioOpts.width, scenarioOpts.totalPoints);
|
||||
|
||||
const logs = generatorFactory.generator((timestamp) => {
|
||||
const entity = faker.helpers.arrayElement(hostEntities);
|
||||
const serviceName = faker.helpers.arrayElement(serviceNames);
|
||||
const level = faker.helpers.arrayElement(LOG_LEVELS);
|
||||
const messages = logMessageGenerators[scenarioOpts.messageGroup](faker);
|
||||
|
||||
return messages.map((message) =>
|
||||
log
|
||||
.createMinimal()
|
||||
.message(message)
|
||||
.logLevel(level)
|
||||
.service(serviceName)
|
||||
.overrides({
|
||||
...entity.fields,
|
||||
labels: {
|
||||
scenario: 'rare',
|
||||
population: scenarioOpts.distribution,
|
||||
},
|
||||
})
|
||||
.timestamp(timestamp)
|
||||
);
|
||||
});
|
||||
|
||||
return [
|
||||
withClient(
|
||||
logsEsClient,
|
||||
logger.perf('generating_logs', () => [logs])
|
||||
),
|
||||
];
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default scenario;
|
||||
|
||||
const logMessageGenerators = {
|
||||
httpAccess: generateUnstructuredLogMessage([unstructuredLogMessageGenerators.httpAccess]),
|
||||
userAuthentication: generateUnstructuredLogMessage([
|
||||
unstructuredLogMessageGenerators.userAuthentication,
|
||||
]),
|
||||
networkEvent: generateUnstructuredLogMessage([unstructuredLogMessageGenerators.networkEvent]),
|
||||
dbOperations: generateUnstructuredLogMessage([unstructuredLogMessageGenerators.dbOperation]),
|
||||
taskOperations: generateUnstructuredLogMessage([
|
||||
unstructuredLogMessageGenerators.taskStatusSuccess,
|
||||
]),
|
||||
degradedOperations: generateUnstructuredLogMessage([
|
||||
unstructuredLogMessageGenerators.taskStatusFailure,
|
||||
]),
|
||||
errorOperations: generateUnstructuredLogMessage([
|
||||
unstructuredLogMessageGenerators.error,
|
||||
unstructuredLogMessageGenerators.restart,
|
||||
]),
|
||||
} satisfies Record<ScenarioOpts['messageGroup'], LogMessageGenerator>;
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 { Faker, faker } from '@faker-js/faker';
|
||||
|
||||
export type LogMessageGenerator = (f: Faker) => string[];
|
||||
|
||||
export const unstructuredLogMessageGenerators = {
|
||||
httpAccess: (f: Faker) => [
|
||||
`${f.internet.ip()} - - [${f.date
|
||||
.past()
|
||||
.toISOString()
|
||||
.replace('T', ' ')
|
||||
.replace(
|
||||
/\..+/,
|
||||
''
|
||||
)}] "${f.internet.httpMethod()} ${f.internet.url()} HTTP/1.1" ${f.helpers.arrayElement([
|
||||
200, 301, 404, 500,
|
||||
])} ${f.number.int({ min: 100, max: 5000 })}`,
|
||||
],
|
||||
dbOperation: (f: Faker) => [
|
||||
`${f.database.engine()}: ${f.database.column()} ${f.helpers.arrayElement([
|
||||
'created',
|
||||
'updated',
|
||||
'deleted',
|
||||
'inserted',
|
||||
])} successfully ${f.number.int({ max: 100000 })} times`,
|
||||
],
|
||||
taskStatusSuccess: (f: Faker) => [
|
||||
`${f.hacker.noun()}: ${f.word.words()} ${f.helpers.arrayElement([
|
||||
'triggered',
|
||||
'executed',
|
||||
'processed',
|
||||
'handled',
|
||||
])} successfully at ${f.date.recent().toISOString()}`,
|
||||
],
|
||||
taskStatusFailure: (f: Faker) => [
|
||||
`${f.hacker.noun()}: ${f.helpers.arrayElement([
|
||||
'triggering',
|
||||
'execution',
|
||||
'processing',
|
||||
'handling',
|
||||
])} of ${f.word.words()} failed at ${f.date.recent().toISOString()}`,
|
||||
],
|
||||
error: (f: Faker) => [
|
||||
`${f.helpers.arrayElement([
|
||||
'Error',
|
||||
'Exception',
|
||||
'Failure',
|
||||
'Crash',
|
||||
'Bug',
|
||||
'Issue',
|
||||
])}: ${f.hacker.phrase()}`,
|
||||
`Stopping ${f.number.int(42)} background tasks...`,
|
||||
'Shutting down process...',
|
||||
],
|
||||
restart: (f: Faker) => {
|
||||
const service = f.database.engine();
|
||||
return [
|
||||
`Restarting ${service}...`,
|
||||
`Waiting for queue to drain...`,
|
||||
`Service ${service} restarted ${f.helpers.arrayElement([
|
||||
'successfully',
|
||||
'with errors',
|
||||
'with warnings',
|
||||
])}`,
|
||||
];
|
||||
},
|
||||
userAuthentication: (f: Faker) => [
|
||||
`User ${f.internet.userName()} ${f.helpers.arrayElement([
|
||||
'logged in',
|
||||
'logged out',
|
||||
'failed to login',
|
||||
])}`,
|
||||
],
|
||||
networkEvent: (f: Faker) => [
|
||||
`Network ${f.helpers.arrayElement([
|
||||
'connection',
|
||||
'disconnection',
|
||||
'data transfer',
|
||||
])} ${f.helpers.arrayElement(['from', 'to'])} ${f.internet.ip()}`,
|
||||
],
|
||||
} satisfies Record<string, LogMessageGenerator>;
|
||||
|
||||
export const generateUnstructuredLogMessage =
|
||||
(generators: LogMessageGenerator[] = Object.values(unstructuredLogMessageGenerators)) =>
|
||||
(f: Faker = faker) =>
|
||||
f.helpers.arrayElement(generators)(f);
|
|
@ -10,6 +10,7 @@
|
|||
"@kbn/apm-synthtrace-client",
|
||||
"@kbn/dev-utils",
|
||||
"@kbn/elastic-agent-utils",
|
||||
"@kbn/zod",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -142,6 +142,7 @@ export const OBSERVABILITY_APM_ENABLE_SERVICE_INVENTORY_TABLE_SEARCH_BAR =
|
|||
'observability:apmEnableServiceInventoryTableSearchBar';
|
||||
export const OBSERVABILITY_LOGS_EXPLORER_ALLOWED_DATA_VIEWS_ID =
|
||||
'observability:logsExplorer:allowedDataViews';
|
||||
export const OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID = 'observability:newLogsOverview';
|
||||
export const OBSERVABILITY_ENTITY_CENTRIC_EXPERIENCE = 'observability:entityCentricExperience';
|
||||
export const OBSERVABILITY_LOGS_DATA_ACCESS_LOG_SOURCES_ID = 'observability:logSources';
|
||||
export const OBSERVABILITY_ENABLE_LOGS_STREAM = 'observability:enableLogsStream';
|
||||
|
|
|
@ -247,6 +247,18 @@ export function getWebpackConfig(
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /node_modules\/@?xstate5\/.*\.js$/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
babelrc: false,
|
||||
envName: worker.dist ? 'production' : 'development',
|
||||
presets: [BABEL_PRESET],
|
||||
plugins: ['@babel/plugin-transform-logical-assignment-operators'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(html|md|txt|tmpl)$/,
|
||||
use: {
|
||||
|
|
|
@ -125,6 +125,17 @@ export default ({ config: storybookConfig }: { config: Configuration }) => {
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /node_modules\/@?xstate5\/.*\.js$/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
babelrc: false,
|
||||
presets: [require.resolve('@kbn/babel-preset/webpack_preset')],
|
||||
plugins: ['@babel/plugin-transform-logical-assignment-operators'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [new IgnoreNotFoundExportPlugin()],
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"type": "shared-browser",
|
||||
"id": "@kbn/xstate-utils",
|
||||
"owner": "@elastic/obs-ux-logs-team"
|
||||
}
|
||||
|
|
88
packages/kbn-xstate-utils/src/console_inspector.ts
Normal file
88
packages/kbn-xstate-utils/src/console_inspector.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 {
|
||||
ActorRefLike,
|
||||
AnyActorRef,
|
||||
InspectedActorEvent,
|
||||
InspectedEventEvent,
|
||||
InspectedSnapshotEvent,
|
||||
InspectionEvent,
|
||||
} from 'xstate5';
|
||||
import { isDevMode } from './dev_tools';
|
||||
|
||||
export const createConsoleInspector = () => {
|
||||
if (!isDevMode()) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
const log = console.info.bind(console);
|
||||
|
||||
const logActorEvent = (actorEvent: InspectedActorEvent) => {
|
||||
if (isActorRef(actorEvent.actorRef)) {
|
||||
log(
|
||||
'✨ %c%s%c is a new actor of type %c%s%c:',
|
||||
...styleAsActor(actorEvent.actorRef.id),
|
||||
...styleAsKeyword(actorEvent.type),
|
||||
actorEvent.actorRef
|
||||
);
|
||||
} else {
|
||||
log('✨ New %c%s%c actor without id:', ...styleAsKeyword(actorEvent.type), actorEvent);
|
||||
}
|
||||
};
|
||||
|
||||
const logEventEvent = (eventEvent: InspectedEventEvent) => {
|
||||
if (isActorRef(eventEvent.actorRef)) {
|
||||
log(
|
||||
'🔔 %c%s%c received event %c%s%c from %c%s%c:',
|
||||
...styleAsActor(eventEvent.actorRef.id),
|
||||
...styleAsKeyword(eventEvent.event.type),
|
||||
...styleAsKeyword(eventEvent.sourceRef?.id),
|
||||
eventEvent
|
||||
);
|
||||
} else {
|
||||
log('🔔 Event', ...styleAsKeyword(eventEvent.event.type), ':', eventEvent);
|
||||
}
|
||||
};
|
||||
|
||||
const logSnapshotEvent = (snapshotEvent: InspectedSnapshotEvent) => {
|
||||
if (isActorRef(snapshotEvent.actorRef)) {
|
||||
log(
|
||||
'📸 %c%s%c updated due to %c%s%c:',
|
||||
...styleAsActor(snapshotEvent.actorRef.id),
|
||||
...styleAsKeyword(snapshotEvent.event.type),
|
||||
snapshotEvent.snapshot
|
||||
);
|
||||
} else {
|
||||
log('📸 Snapshot due to %c%s%c:', ...styleAsKeyword(snapshotEvent.event.type), snapshotEvent);
|
||||
}
|
||||
};
|
||||
|
||||
return (inspectionEvent: InspectionEvent) => {
|
||||
if (inspectionEvent.type === '@xstate.actor') {
|
||||
logActorEvent(inspectionEvent);
|
||||
} else if (inspectionEvent.type === '@xstate.event') {
|
||||
logEventEvent(inspectionEvent);
|
||||
} else if (inspectionEvent.type === '@xstate.snapshot') {
|
||||
logSnapshotEvent(inspectionEvent);
|
||||
} else {
|
||||
log(`❓ Received inspection event:`, inspectionEvent);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const isActorRef = (actorRefLike: ActorRefLike): actorRefLike is AnyActorRef =>
|
||||
'id' in actorRefLike;
|
||||
|
||||
const keywordStyle = 'font-weight: bold';
|
||||
const styleAsKeyword = (value: any) => [keywordStyle, value, ''] as const;
|
||||
|
||||
const actorStyle = 'font-weight: bold; text-decoration: underline';
|
||||
const styleAsActor = (value: any) => [actorStyle, value, ''] as const;
|
|
@ -9,5 +9,6 @@
|
|||
|
||||
export * from './actions';
|
||||
export * from './dev_tools';
|
||||
export * from './console_inspector';
|
||||
export * from './notification_channel';
|
||||
export * from './types';
|
||||
|
|
|
@ -705,4 +705,10 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
|
|||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
},
|
||||
'observability:newLogsOverview': {
|
||||
type: 'boolean',
|
||||
_meta: {
|
||||
description: 'Enable the new logs overview component.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -56,6 +56,7 @@ export interface UsageStats {
|
|||
'observability:logsExplorer:allowedDataViews': string[];
|
||||
'observability:logSources': string[];
|
||||
'observability:enableLogsStream': boolean;
|
||||
'observability:newLogsOverview': boolean;
|
||||
'observability:aiAssistantSimulatedFunctionCalling': boolean;
|
||||
'observability:aiAssistantSearchConnectorIndexPattern': string;
|
||||
'visualization:heatmap:maxBuckets': number;
|
||||
|
|
|
@ -10768,6 +10768,12 @@
|
|||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"observability:newLogsOverview": {
|
||||
"type": "boolean",
|
||||
"_meta": {
|
||||
"description": "Enable the new logs overview component."
|
||||
}
|
||||
},
|
||||
"observability:searchExcludedDataTiers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
|
@ -1298,6 +1298,8 @@
|
|||
"@kbn/observability-get-padded-alert-time-range-util/*": ["x-pack/packages/observability/get_padded_alert_time_range_util/*"],
|
||||
"@kbn/observability-logs-explorer-plugin": ["x-pack/plugins/observability_solution/observability_logs_explorer"],
|
||||
"@kbn/observability-logs-explorer-plugin/*": ["x-pack/plugins/observability_solution/observability_logs_explorer/*"],
|
||||
"@kbn/observability-logs-overview": ["x-pack/packages/observability/logs_overview"],
|
||||
"@kbn/observability-logs-overview/*": ["x-pack/packages/observability/logs_overview/*"],
|
||||
"@kbn/observability-onboarding-e2e": ["x-pack/plugins/observability_solution/observability_onboarding/e2e"],
|
||||
"@kbn/observability-onboarding-e2e/*": ["x-pack/plugins/observability_solution/observability_onboarding/e2e/*"],
|
||||
"@kbn/observability-onboarding-plugin": ["x-pack/plugins/observability_solution/observability_onboarding"],
|
||||
|
|
|
@ -95,6 +95,9 @@
|
|||
"xpack.observabilityLogsExplorer": "plugins/observability_solution/observability_logs_explorer",
|
||||
"xpack.observability_onboarding": "plugins/observability_solution/observability_onboarding",
|
||||
"xpack.observabilityShared": "plugins/observability_solution/observability_shared",
|
||||
"xpack.observabilityLogsOverview": [
|
||||
"packages/observability/logs_overview/src/components"
|
||||
],
|
||||
"xpack.osquery": ["plugins/osquery"],
|
||||
"xpack.painlessLab": "plugins/painless_lab",
|
||||
"xpack.profiling": ["plugins/observability_solution/profiling"],
|
||||
|
|
3
x-pack/packages/observability/logs_overview/README.md
Normal file
3
x-pack/packages/observability/logs_overview/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/observability-logs-overview
|
||||
|
||||
Empty package generated by @kbn/generate
|
21
x-pack/packages/observability/logs_overview/index.ts
Normal file
21
x-pack/packages/observability/logs_overview/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 {
|
||||
LogsOverview,
|
||||
LogsOverviewErrorContent,
|
||||
LogsOverviewLoadingContent,
|
||||
type LogsOverviewDependencies,
|
||||
type LogsOverviewErrorContentProps,
|
||||
type LogsOverviewProps,
|
||||
} from './src/components/logs_overview';
|
||||
export type {
|
||||
DataViewLogsSourceConfiguration,
|
||||
IndexNameLogsSourceConfiguration,
|
||||
LogsSourceConfiguration,
|
||||
SharedSettingLogsSourceConfiguration,
|
||||
} from './src/utils/logs_source';
|
12
x-pack/packages/observability/logs_overview/jest.config.js
Normal file
12
x-pack/packages/observability/logs_overview/jest.config.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../..',
|
||||
roots: ['<rootDir>/x-pack/packages/observability/logs_overview'],
|
||||
};
|
5
x-pack/packages/observability/logs_overview/kibana.jsonc
Normal file
5
x-pack/packages/observability/logs_overview/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-browser",
|
||||
"id": "@kbn/observability-logs-overview",
|
||||
"owner": "@elastic/obs-ux-logs-team"
|
||||
}
|
7
x-pack/packages/observability/logs_overview/package.json
Normal file
7
x-pack/packages/observability/logs_overview/package.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "@kbn/observability-logs-overview",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "Elastic License 2.0",
|
||||
"sideEffects": false
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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/types';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import type { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
|
||||
import { FilterStateStore, buildCustomFilter } from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getRouterLinkProps } from '@kbn/router-utils';
|
||||
import type { SharePluginStart } from '@kbn/share-plugin/public';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import type { IndexNameLogsSourceConfiguration } from '../../utils/logs_source';
|
||||
|
||||
export interface DiscoverLinkProps {
|
||||
documentFilters?: QueryDslQueryContainer[];
|
||||
logsSource: IndexNameLogsSourceConfiguration;
|
||||
timeRange: {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
dependencies: DiscoverLinkDependencies;
|
||||
}
|
||||
|
||||
export interface DiscoverLinkDependencies {
|
||||
share: SharePluginStart;
|
||||
}
|
||||
|
||||
export const DiscoverLink = React.memo(
|
||||
({ dependencies: { share }, documentFilters, logsSource, timeRange }: DiscoverLinkProps) => {
|
||||
const discoverLocatorParams = useMemo<DiscoverAppLocatorParams>(
|
||||
() => ({
|
||||
dataViewSpec: {
|
||||
id: logsSource.indexName,
|
||||
name: logsSource.indexName,
|
||||
title: logsSource.indexName,
|
||||
timeFieldName: logsSource.timestampField,
|
||||
},
|
||||
timeRange: {
|
||||
from: timeRange.start,
|
||||
to: timeRange.end,
|
||||
},
|
||||
filters: documentFilters?.map((filter) =>
|
||||
buildCustomFilter(
|
||||
logsSource.indexName,
|
||||
filter,
|
||||
false,
|
||||
false,
|
||||
categorizedLogsFilterLabel,
|
||||
FilterStateStore.APP_STATE
|
||||
)
|
||||
),
|
||||
}),
|
||||
[
|
||||
documentFilters,
|
||||
logsSource.indexName,
|
||||
logsSource.timestampField,
|
||||
timeRange.end,
|
||||
timeRange.start,
|
||||
]
|
||||
);
|
||||
|
||||
const discoverLocator = useMemo(
|
||||
() => share.url.locators.get<DiscoverAppLocatorParams>('DISCOVER_APP_LOCATOR'),
|
||||
[share.url.locators]
|
||||
);
|
||||
|
||||
const discoverUrl = useMemo(
|
||||
() => discoverLocator?.getRedirectUrl(discoverLocatorParams),
|
||||
[discoverLocatorParams, discoverLocator]
|
||||
);
|
||||
|
||||
const navigateToDiscover = useCallback(() => {
|
||||
discoverLocator?.navigate(discoverLocatorParams);
|
||||
}, [discoverLocatorParams, discoverLocator]);
|
||||
|
||||
const discoverLinkProps = getRouterLinkProps({
|
||||
href: discoverUrl,
|
||||
onClick: navigateToDiscover,
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiButton
|
||||
{...discoverLinkProps}
|
||||
color="primary"
|
||||
iconType="discoverApp"
|
||||
data-test-subj="logsExplorerDiscoverFallbackLink"
|
||||
>
|
||||
{discoverLinkTitle}
|
||||
</EuiButton>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const discoverLinkTitle = i18n.translate(
|
||||
'xpack.observabilityLogsOverview.discoverLinkTitle',
|
||||
{
|
||||
defaultMessage: 'Open in Discover',
|
||||
}
|
||||
);
|
||||
|
||||
export const categorizedLogsFilterLabel = i18n.translate(
|
||||
'xpack.observabilityLogsOverview.categorizedLogsFilterLabel',
|
||||
{
|
||||
defaultMessage: 'Categorized log entries',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 * from './discover_link';
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 * from './log_categories';
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { ISearchGeneric } from '@kbn/search-types';
|
||||
import { createConsoleInspector } from '@kbn/xstate-utils';
|
||||
import { useMachine } from '@xstate5/react';
|
||||
import React, { useCallback } from 'react';
|
||||
import {
|
||||
categorizeLogsService,
|
||||
createCategorizeLogsServiceImplementations,
|
||||
} from '../../services/categorize_logs_service';
|
||||
import { IndexNameLogsSourceConfiguration } from '../../utils/logs_source';
|
||||
import { LogCategoriesErrorContent } from './log_categories_error_content';
|
||||
import { LogCategoriesLoadingContent } from './log_categories_loading_content';
|
||||
import {
|
||||
LogCategoriesResultContent,
|
||||
LogCategoriesResultContentDependencies,
|
||||
} from './log_categories_result_content';
|
||||
|
||||
export interface LogCategoriesProps {
|
||||
dependencies: LogCategoriesDependencies;
|
||||
documentFilters?: QueryDslQueryContainer[];
|
||||
logsSource: IndexNameLogsSourceConfiguration;
|
||||
// The time range could be made optional if we want to support an internal
|
||||
// time range picker
|
||||
timeRange: {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type LogCategoriesDependencies = LogCategoriesResultContentDependencies & {
|
||||
search: ISearchGeneric;
|
||||
};
|
||||
|
||||
export const LogCategories: React.FC<LogCategoriesProps> = ({
|
||||
dependencies,
|
||||
documentFilters = [],
|
||||
logsSource,
|
||||
timeRange,
|
||||
}) => {
|
||||
const [categorizeLogsServiceState, sendToCategorizeLogsService] = useMachine(
|
||||
categorizeLogsService.provide(
|
||||
createCategorizeLogsServiceImplementations({ search: dependencies.search })
|
||||
),
|
||||
{
|
||||
inspect: consoleInspector,
|
||||
input: {
|
||||
index: logsSource.indexName,
|
||||
startTimestamp: timeRange.start,
|
||||
endTimestamp: timeRange.end,
|
||||
timeField: logsSource.timestampField,
|
||||
messageField: logsSource.messageField,
|
||||
documentFilters,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const cancelOperation = useCallback(() => {
|
||||
sendToCategorizeLogsService({
|
||||
type: 'cancel',
|
||||
});
|
||||
}, [sendToCategorizeLogsService]);
|
||||
|
||||
if (categorizeLogsServiceState.matches('done')) {
|
||||
return (
|
||||
<LogCategoriesResultContent
|
||||
dependencies={dependencies}
|
||||
documentFilters={documentFilters}
|
||||
logCategories={categorizeLogsServiceState.context.categories}
|
||||
logsSource={logsSource}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
);
|
||||
} else if (categorizeLogsServiceState.matches('failed')) {
|
||||
return <LogCategoriesErrorContent error={categorizeLogsServiceState.context.error} />;
|
||||
} else if (categorizeLogsServiceState.matches('countingDocuments')) {
|
||||
return <LogCategoriesLoadingContent onCancel={cancelOperation} stage="counting" />;
|
||||
} else if (
|
||||
categorizeLogsServiceState.matches('fetchingSampledCategories') ||
|
||||
categorizeLogsServiceState.matches('fetchingRemainingCategories')
|
||||
) {
|
||||
return <LogCategoriesLoadingContent onCancel={cancelOperation} stage="categorizing" />;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const consoleInspector = createConsoleInspector();
|
|
@ -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 type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import type { SharePluginStart } from '@kbn/share-plugin/public';
|
||||
import React from 'react';
|
||||
import type { IndexNameLogsSourceConfiguration } from '../../utils/logs_source';
|
||||
import { DiscoverLink } from '../discover_link';
|
||||
|
||||
export interface LogCategoriesControlBarProps {
|
||||
documentFilters?: QueryDslQueryContainer[];
|
||||
logsSource: IndexNameLogsSourceConfiguration;
|
||||
timeRange: {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
dependencies: LogCategoriesControlBarDependencies;
|
||||
}
|
||||
|
||||
export interface LogCategoriesControlBarDependencies {
|
||||
share: SharePluginStart;
|
||||
}
|
||||
|
||||
export const LogCategoriesControlBar: React.FC<LogCategoriesControlBarProps> = React.memo(
|
||||
({ dependencies, documentFilters, logsSource, timeRange }) => {
|
||||
return (
|
||||
<EuiFlexGroup direction="row" justifyContent="flexEnd" alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<DiscoverLink
|
||||
dependencies={dependencies}
|
||||
documentFilters={documentFilters}
|
||||
logsSource={logsSource}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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 { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
export interface LogCategoriesErrorContentProps {
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export const LogCategoriesErrorContent: React.FC<LogCategoriesErrorContentProps> = ({ error }) => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
color="danger"
|
||||
iconType="error"
|
||||
title={<h2>{logsOverviewErrorTitle}</h2>}
|
||||
body={
|
||||
<EuiCodeBlock className="eui-textLeft" whiteSpace="pre">
|
||||
<p>{error?.stack ?? error?.toString() ?? unknownErrorDescription}</p>
|
||||
</EuiCodeBlock>
|
||||
}
|
||||
layout="vertical"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const logsOverviewErrorTitle = i18n.translate(
|
||||
'xpack.observabilityLogsOverview.logCategories.errorTitle',
|
||||
{
|
||||
defaultMessage: 'Failed to categorize logs',
|
||||
}
|
||||
);
|
||||
|
||||
const unknownErrorDescription = i18n.translate(
|
||||
'xpack.observabilityLogsOverview.logCategories.unknownErrorDescription',
|
||||
{
|
||||
defaultMessage: 'An unspecified error occurred.',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiDataGrid,
|
||||
EuiDataGridColumnSortingConfig,
|
||||
EuiDataGridPaginationProps,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { createConsoleInspector } from '@kbn/xstate-utils';
|
||||
import { useMachine } from '@xstate5/react';
|
||||
import _ from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
import { assign, setup } from 'xstate5';
|
||||
import { LogCategory } from '../../types';
|
||||
import {
|
||||
LogCategoriesGridCellDependencies,
|
||||
LogCategoriesGridColumnId,
|
||||
createCellContext,
|
||||
logCategoriesGridColumnIds,
|
||||
logCategoriesGridColumns,
|
||||
renderLogCategoriesGridCell,
|
||||
} from './log_categories_grid_cell';
|
||||
|
||||
export interface LogCategoriesGridProps {
|
||||
dependencies: LogCategoriesGridDependencies;
|
||||
logCategories: LogCategory[];
|
||||
}
|
||||
|
||||
export type LogCategoriesGridDependencies = LogCategoriesGridCellDependencies;
|
||||
|
||||
export const LogCategoriesGrid: React.FC<LogCategoriesGridProps> = ({
|
||||
dependencies,
|
||||
logCategories,
|
||||
}) => {
|
||||
const [gridState, dispatchGridEvent] = useMachine(gridStateService, {
|
||||
input: {
|
||||
visibleColumns: logCategoriesGridColumns.map(({ id }) => id),
|
||||
},
|
||||
inspect: consoleInspector,
|
||||
});
|
||||
|
||||
const sortedLogCategories = useMemo(() => {
|
||||
const sortingCriteria = gridState.context.sortingColumns.map(
|
||||
({ id, direction }): [(logCategory: LogCategory) => any, 'asc' | 'desc'] => {
|
||||
switch (id) {
|
||||
case 'count':
|
||||
return [(logCategory: LogCategory) => logCategory.documentCount, direction];
|
||||
case 'change_type':
|
||||
// TODO: use better sorting weight for change types
|
||||
return [(logCategory: LogCategory) => logCategory.change.type, direction];
|
||||
case 'change_time':
|
||||
return [
|
||||
(logCategory: LogCategory) =>
|
||||
'timestamp' in logCategory.change ? logCategory.change.timestamp ?? '' : '',
|
||||
direction,
|
||||
];
|
||||
default:
|
||||
return [_.identity, direction];
|
||||
}
|
||||
}
|
||||
);
|
||||
return _.orderBy(
|
||||
logCategories,
|
||||
sortingCriteria.map(([accessor]) => accessor),
|
||||
sortingCriteria.map(([, direction]) => direction)
|
||||
);
|
||||
}, [gridState.context.sortingColumns, logCategories]);
|
||||
|
||||
return (
|
||||
<EuiDataGrid
|
||||
aria-label={logCategoriesGridLabel}
|
||||
columns={logCategoriesGridColumns}
|
||||
columnVisibility={{
|
||||
visibleColumns: gridState.context.visibleColumns,
|
||||
setVisibleColumns: (visibleColumns) =>
|
||||
dispatchGridEvent({ type: 'changeVisibleColumns', visibleColumns }),
|
||||
}}
|
||||
cellContext={createCellContext(sortedLogCategories, dependencies)}
|
||||
pagination={{
|
||||
...gridState.context.pagination,
|
||||
onChangeItemsPerPage: (pageSize) => dispatchGridEvent({ type: 'changePageSize', pageSize }),
|
||||
onChangePage: (pageIndex) => dispatchGridEvent({ type: 'changePageIndex', pageIndex }),
|
||||
}}
|
||||
renderCellValue={renderLogCategoriesGridCell}
|
||||
rowCount={sortedLogCategories.length}
|
||||
sorting={{
|
||||
columns: gridState.context.sortingColumns,
|
||||
onSort: (sortingColumns) =>
|
||||
dispatchGridEvent({ type: 'changeSortingColumns', sortingColumns }),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const gridStateService = setup({
|
||||
types: {
|
||||
context: {} as {
|
||||
visibleColumns: string[];
|
||||
pagination: Pick<EuiDataGridPaginationProps, 'pageIndex' | 'pageSize' | 'pageSizeOptions'>;
|
||||
sortingColumns: LogCategoriesGridSortingConfig[];
|
||||
},
|
||||
events: {} as
|
||||
| {
|
||||
type: 'changePageSize';
|
||||
pageSize: number;
|
||||
}
|
||||
| {
|
||||
type: 'changePageIndex';
|
||||
pageIndex: number;
|
||||
}
|
||||
| {
|
||||
type: 'changeSortingColumns';
|
||||
sortingColumns: EuiDataGridColumnSortingConfig[];
|
||||
}
|
||||
| {
|
||||
type: 'changeVisibleColumns';
|
||||
visibleColumns: string[];
|
||||
},
|
||||
input: {} as {
|
||||
visibleColumns: string[];
|
||||
},
|
||||
},
|
||||
}).createMachine({
|
||||
id: 'logCategoriesGridState',
|
||||
context: ({ input }) => ({
|
||||
visibleColumns: input.visibleColumns,
|
||||
pagination: { pageIndex: 0, pageSize: 20, pageSizeOptions: [10, 20, 50] },
|
||||
sortingColumns: [{ id: 'change_time', direction: 'desc' }],
|
||||
}),
|
||||
on: {
|
||||
changePageSize: {
|
||||
actions: assign(({ context, event }) => ({
|
||||
pagination: {
|
||||
...context.pagination,
|
||||
pageIndex: 0,
|
||||
pageSize: event.pageSize,
|
||||
},
|
||||
})),
|
||||
},
|
||||
changePageIndex: {
|
||||
actions: assign(({ context, event }) => ({
|
||||
pagination: {
|
||||
...context.pagination,
|
||||
pageIndex: event.pageIndex,
|
||||
},
|
||||
})),
|
||||
},
|
||||
changeSortingColumns: {
|
||||
actions: assign(({ event }) => ({
|
||||
sortingColumns: event.sortingColumns.filter(
|
||||
(sortingConfig): sortingConfig is LogCategoriesGridSortingConfig =>
|
||||
(logCategoriesGridColumnIds as string[]).includes(sortingConfig.id)
|
||||
),
|
||||
})),
|
||||
},
|
||||
changeVisibleColumns: {
|
||||
actions: assign(({ event }) => ({
|
||||
visibleColumns: event.visibleColumns,
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const consoleInspector = createConsoleInspector();
|
||||
|
||||
const logCategoriesGridLabel = i18n.translate(
|
||||
'xpack.observabilityLogsOverview.logCategoriesGrid.euiDataGrid.logCategoriesLabel',
|
||||
{ defaultMessage: 'Log categories' }
|
||||
);
|
||||
|
||||
interface TypedEuiDataGridColumnSortingConfig<ColumnId extends string>
|
||||
extends EuiDataGridColumnSortingConfig {
|
||||
id: ColumnId;
|
||||
}
|
||||
|
||||
type LogCategoriesGridSortingConfig =
|
||||
TypedEuiDataGridColumnSortingConfig<LogCategoriesGridColumnId>;
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 { EuiDataGridColumn, RenderCellValue } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { LogCategory } from '../../types';
|
||||
import {
|
||||
LogCategoriesGridChangeTimeCell,
|
||||
LogCategoriesGridChangeTimeCellDependencies,
|
||||
logCategoriesGridChangeTimeColumn,
|
||||
} from './log_categories_grid_change_time_cell';
|
||||
import {
|
||||
LogCategoriesGridChangeTypeCell,
|
||||
logCategoriesGridChangeTypeColumn,
|
||||
} from './log_categories_grid_change_type_cell';
|
||||
import {
|
||||
LogCategoriesGridCountCell,
|
||||
logCategoriesGridCountColumn,
|
||||
} from './log_categories_grid_count_cell';
|
||||
import {
|
||||
LogCategoriesGridHistogramCell,
|
||||
LogCategoriesGridHistogramCellDependencies,
|
||||
logCategoriesGridHistoryColumn,
|
||||
} from './log_categories_grid_histogram_cell';
|
||||
import {
|
||||
LogCategoriesGridPatternCell,
|
||||
logCategoriesGridPatternColumn,
|
||||
} from './log_categories_grid_pattern_cell';
|
||||
|
||||
export interface LogCategoriesGridCellContext {
|
||||
dependencies: LogCategoriesGridCellDependencies;
|
||||
logCategories: LogCategory[];
|
||||
}
|
||||
|
||||
export type LogCategoriesGridCellDependencies = LogCategoriesGridHistogramCellDependencies &
|
||||
LogCategoriesGridChangeTimeCellDependencies;
|
||||
|
||||
export const renderLogCategoriesGridCell: RenderCellValue = ({
|
||||
rowIndex,
|
||||
columnId,
|
||||
isExpanded,
|
||||
...rest
|
||||
}) => {
|
||||
const { dependencies, logCategories } = getCellContext(rest);
|
||||
|
||||
const logCategory = logCategories[rowIndex];
|
||||
|
||||
switch (columnId as LogCategoriesGridColumnId) {
|
||||
case 'pattern':
|
||||
return <LogCategoriesGridPatternCell logCategory={logCategory} />;
|
||||
case 'count':
|
||||
return <LogCategoriesGridCountCell logCategory={logCategory} />;
|
||||
case 'history':
|
||||
return (
|
||||
<LogCategoriesGridHistogramCell dependencies={dependencies} logCategory={logCategory} />
|
||||
);
|
||||
case 'change_type':
|
||||
return <LogCategoriesGridChangeTypeCell logCategory={logCategory} />;
|
||||
case 'change_time':
|
||||
return (
|
||||
<LogCategoriesGridChangeTimeCell dependencies={dependencies} logCategory={logCategory} />
|
||||
);
|
||||
default:
|
||||
return <>-</>;
|
||||
}
|
||||
};
|
||||
|
||||
export const logCategoriesGridColumns = [
|
||||
logCategoriesGridPatternColumn,
|
||||
logCategoriesGridCountColumn,
|
||||
logCategoriesGridChangeTypeColumn,
|
||||
logCategoriesGridChangeTimeColumn,
|
||||
logCategoriesGridHistoryColumn,
|
||||
] satisfies EuiDataGridColumn[];
|
||||
|
||||
export const logCategoriesGridColumnIds = logCategoriesGridColumns.map(({ id }) => id);
|
||||
|
||||
export type LogCategoriesGridColumnId = (typeof logCategoriesGridColumns)[number]['id'];
|
||||
|
||||
const cellContextKey = 'cellContext';
|
||||
|
||||
const getCellContext = (cellContext: object): LogCategoriesGridCellContext =>
|
||||
(cellContextKey in cellContext
|
||||
? cellContext[cellContextKey]
|
||||
: {}) as LogCategoriesGridCellContext;
|
||||
|
||||
export const createCellContext = (
|
||||
logCategories: LogCategory[],
|
||||
dependencies: LogCategoriesGridCellDependencies
|
||||
): { [cellContextKey]: LogCategoriesGridCellContext } => ({
|
||||
[cellContextKey]: {
|
||||
dependencies,
|
||||
logCategories,
|
||||
},
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { EuiDataGridColumn } from '@elastic/eui';
|
||||
import { SettingsStart } from '@kbn/core-ui-settings-browser';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import moment from 'moment';
|
||||
import React, { useMemo } from 'react';
|
||||
import { LogCategory } from '../../types';
|
||||
|
||||
export const logCategoriesGridChangeTimeColumn = {
|
||||
id: 'change_time' as const,
|
||||
display: i18n.translate(
|
||||
'xpack.observabilityLogsOverview.logCategoriesGrid.changeTimeColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Change at',
|
||||
}
|
||||
),
|
||||
isSortable: true,
|
||||
initialWidth: 220,
|
||||
schema: 'datetime',
|
||||
} satisfies EuiDataGridColumn;
|
||||
|
||||
export interface LogCategoriesGridChangeTimeCellProps {
|
||||
dependencies: LogCategoriesGridChangeTimeCellDependencies;
|
||||
logCategory: LogCategory;
|
||||
}
|
||||
|
||||
export interface LogCategoriesGridChangeTimeCellDependencies {
|
||||
uiSettings: SettingsStart;
|
||||
}
|
||||
|
||||
export const LogCategoriesGridChangeTimeCell: React.FC<LogCategoriesGridChangeTimeCellProps> = ({
|
||||
dependencies,
|
||||
logCategory,
|
||||
}) => {
|
||||
const dateFormat = useMemo(
|
||||
() => dependencies.uiSettings.client.get('dateFormat'),
|
||||
[dependencies.uiSettings.client]
|
||||
);
|
||||
if (!('timestamp' in logCategory.change && logCategory.change.timestamp != null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dateFormat) {
|
||||
return <>{moment(logCategory.change.timestamp).format(dateFormat)}</>;
|
||||
} else {
|
||||
return <>{logCategory.change.timestamp}</>;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiBadge, EuiDataGridColumn } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { LogCategory } from '../../types';
|
||||
|
||||
export const logCategoriesGridChangeTypeColumn = {
|
||||
id: 'change_type' as const,
|
||||
display: i18n.translate(
|
||||
'xpack.observabilityLogsOverview.logCategoriesGrid.changeTypeColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Change type',
|
||||
}
|
||||
),
|
||||
isSortable: true,
|
||||
initialWidth: 110,
|
||||
} satisfies EuiDataGridColumn;
|
||||
|
||||
export interface LogCategoriesGridChangeTypeCellProps {
|
||||
logCategory: LogCategory;
|
||||
}
|
||||
|
||||
export const LogCategoriesGridChangeTypeCell: React.FC<LogCategoriesGridChangeTypeCellProps> = ({
|
||||
logCategory,
|
||||
}) => {
|
||||
switch (logCategory.change.type) {
|
||||
case 'dip':
|
||||
return <EuiBadge color="hollow">{dipBadgeLabel}</EuiBadge>;
|
||||
case 'spike':
|
||||
return <EuiBadge color="hollow">{spikeBadgeLabel}</EuiBadge>;
|
||||
case 'step':
|
||||
return <EuiBadge color="hollow">{stepBadgeLabel}</EuiBadge>;
|
||||
case 'distribution':
|
||||
return <EuiBadge color="hollow">{distributionBadgeLabel}</EuiBadge>;
|
||||
case 'rare':
|
||||
return <EuiBadge color="hollow">{rareBadgeLabel}</EuiBadge>;
|
||||
case 'trend':
|
||||
return <EuiBadge color="hollow">{trendBadgeLabel}</EuiBadge>;
|
||||
case 'other':
|
||||
return <EuiBadge color="hollow">{otherBadgeLabel}</EuiBadge>;
|
||||
case 'none':
|
||||
return <>-</>;
|
||||
default:
|
||||
return <EuiBadge color="hollow">{unknownBadgeLabel}</EuiBadge>;
|
||||
}
|
||||
};
|
||||
|
||||
const dipBadgeLabel = i18n.translate(
|
||||
'xpack.observabilityLogsOverview.logCategories.dipChangeTypeBadgeLabel',
|
||||
{
|
||||
defaultMessage: 'Dip',
|
||||
}
|
||||
);
|
||||
|
||||
const spikeBadgeLabel = i18n.translate(
|
||||
'xpack.observabilityLogsOverview.logCategories.spikeChangeTypeBadgeLabel',
|
||||
{
|
||||
defaultMessage: 'Spike',
|
||||
}
|
||||
);
|
||||
|
||||
const stepBadgeLabel = i18n.translate(
|
||||
'xpack.observabilityLogsOverview.logCategories.spikeChangeTypeBadgeLabel',
|
||||
{
|
||||
defaultMessage: 'Step',
|
||||
}
|
||||
);
|
||||
|
||||
const distributionBadgeLabel = i18n.translate(
|
||||
'xpack.observabilityLogsOverview.logCategories.distributionChangeTypeBadgeLabel',
|
||||
{
|
||||
defaultMessage: 'Distribution',
|
||||
}
|
||||
);
|
||||
|
||||
const trendBadgeLabel = i18n.translate(
|
||||
'xpack.observabilityLogsOverview.logCategories.spikeChangeTypeBadgeLabel',
|
||||
{
|
||||
defaultMessage: 'Trend',
|
||||
}
|
||||
);
|
||||
|
||||
const otherBadgeLabel = i18n.translate(
|
||||
'xpack.observabilityLogsOverview.logCategories.otherChangeTypeBadgeLabel',
|
||||
{
|
||||
defaultMessage: 'Other',
|
||||
}
|
||||
);
|
||||
|
||||
const unknownBadgeLabel = i18n.translate(
|
||||
'xpack.observabilityLogsOverview.logCategories.unknownChangeTypeBadgeLabel',
|
||||
{
|
||||
defaultMessage: 'Unknown',
|
||||
}
|
||||
);
|
||||
|
||||
const rareBadgeLabel = i18n.translate(
|
||||
'xpack.observabilityLogsOverview.logCategories.rareChangeTypeBadgeLabel',
|
||||
{
|
||||
defaultMessage: 'Rare',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { EuiDataGridColumn } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedNumber } from '@kbn/i18n-react';
|
||||
import React from 'react';
|
||||
import { LogCategory } from '../../types';
|
||||
|
||||
export const logCategoriesGridCountColumn = {
|
||||
id: 'count' as const,
|
||||
display: i18n.translate('xpack.observabilityLogsOverview.logCategoriesGrid.countColumnLabel', {
|
||||
defaultMessage: 'Events',
|
||||
}),
|
||||
isSortable: true,
|
||||
schema: 'numeric',
|
||||
initialWidth: 100,
|
||||
} satisfies EuiDataGridColumn;
|
||||
|
||||
export interface LogCategoriesGridCountCellProps {
|
||||
logCategory: LogCategory;
|
||||
}
|
||||
|
||||
export const LogCategoriesGridCountCell: React.FC<LogCategoriesGridCountCellProps> = ({
|
||||
logCategory,
|
||||
}) => {
|
||||
return <FormattedNumber value={logCategory.documentCount} />;
|
||||
};
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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 {
|
||||
BarSeries,
|
||||
Chart,
|
||||
LineAnnotation,
|
||||
LineAnnotationStyle,
|
||||
PartialTheme,
|
||||
Settings,
|
||||
Tooltip,
|
||||
TooltipType,
|
||||
} from '@elastic/charts';
|
||||
import { EuiDataGridColumn } from '@elastic/eui';
|
||||
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RecursivePartial } from '@kbn/utility-types';
|
||||
import React from 'react';
|
||||
import { LogCategory, LogCategoryHistogramBucket } from '../../types';
|
||||
|
||||
export const logCategoriesGridHistoryColumn = {
|
||||
id: 'history' as const,
|
||||
display: i18n.translate(
|
||||
'xpack.observabilityLogsOverview.logCategoriesGrid.histogramColumnLabel',
|
||||
{
|
||||
defaultMessage: 'Timeline',
|
||||
}
|
||||
),
|
||||
isSortable: false,
|
||||
initialWidth: 250,
|
||||
isExpandable: false,
|
||||
} satisfies EuiDataGridColumn;
|
||||
|
||||
export interface LogCategoriesGridHistogramCellProps {
|
||||
dependencies: LogCategoriesGridHistogramCellDependencies;
|
||||
logCategory: LogCategory;
|
||||
}
|
||||
|
||||
export interface LogCategoriesGridHistogramCellDependencies {
|
||||
charts: ChartsPluginStart;
|
||||
}
|
||||
|
||||
export const LogCategoriesGridHistogramCell: React.FC<LogCategoriesGridHistogramCellProps> = ({
|
||||
dependencies: { charts },
|
||||
logCategory,
|
||||
}) => {
|
||||
const baseTheme = charts.theme.useChartsBaseTheme();
|
||||
const sparklineTheme = charts.theme.useSparklineOverrides();
|
||||
|
||||
return (
|
||||
<Chart>
|
||||
<Tooltip type={TooltipType.None} />
|
||||
<Settings
|
||||
baseTheme={baseTheme}
|
||||
showLegend={false}
|
||||
theme={[sparklineTheme, localThemeOverrides]}
|
||||
/>
|
||||
<BarSeries
|
||||
data={logCategory.histogram}
|
||||
id="documentCount"
|
||||
xAccessor={timestampAccessor}
|
||||
xScaleType="time"
|
||||
yAccessors={['documentCount']}
|
||||
yScaleType="linear"
|
||||
enableHistogramMode
|
||||
/>
|
||||
{'timestamp' in logCategory.change && logCategory.change.timestamp != null ? (
|
||||
<LineAnnotation
|
||||
id="change"
|
||||
dataValues={[{ dataValue: new Date(logCategory.change.timestamp).getTime() }]}
|
||||
domainType="xDomain"
|
||||
style={annotationStyle}
|
||||
/>
|
||||
) : null}
|
||||
</Chart>
|
||||
);
|
||||
};
|
||||
|
||||
const localThemeOverrides: PartialTheme = {
|
||||
scales: {
|
||||
histogramPadding: 0.1,
|
||||
},
|
||||
background: {
|
||||
color: 'transparent',
|
||||
},
|
||||
};
|
||||
|
||||
const annotationStyle: RecursivePartial<LineAnnotationStyle> = {
|
||||
line: {
|
||||
strokeWidth: 2,
|
||||
},
|
||||
};
|
||||
|
||||
const timestampAccessor = (histogram: LogCategoryHistogramBucket) =>
|
||||
new Date(histogram.timestamp).getTime();
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 { EuiDataGridColumn, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
import { LogCategory } from '../../types';
|
||||
|
||||
export const logCategoriesGridPatternColumn = {
|
||||
id: 'pattern' as const,
|
||||
display: i18n.translate('xpack.observabilityLogsOverview.logCategoriesGrid.patternColumnLabel', {
|
||||
defaultMessage: 'Pattern',
|
||||
}),
|
||||
isSortable: false,
|
||||
schema: 'string',
|
||||
} satisfies EuiDataGridColumn;
|
||||
|
||||
export interface LogCategoriesGridPatternCellProps {
|
||||
logCategory: LogCategory;
|
||||
}
|
||||
|
||||
export const LogCategoriesGridPatternCell: React.FC<LogCategoriesGridPatternCellProps> = ({
|
||||
logCategory,
|
||||
}) => {
|
||||
const theme = useEuiTheme();
|
||||
const { euiTheme } = theme;
|
||||
const termsList = useMemo(() => logCategory.terms.split(' '), [logCategory.terms]);
|
||||
|
||||
const commonStyle = css`
|
||||
display: inline-block;
|
||||
font-family: ${euiTheme.font.familyCode};
|
||||
margin-right: ${euiTheme.size.xs};
|
||||
`;
|
||||
|
||||
const termStyle = css`
|
||||
${commonStyle};
|
||||
`;
|
||||
|
||||
const separatorStyle = css`
|
||||
${commonStyle};
|
||||
color: ${euiTheme.colors.successText};
|
||||
`;
|
||||
|
||||
return (
|
||||
<pre>
|
||||
<div css={separatorStyle}>*</div>
|
||||
{termsList.map((term, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<div css={termStyle}>{term}</div>
|
||||
<div css={separatorStyle}>*</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</pre>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export interface LogCategoriesLoadingContentProps {
|
||||
onCancel?: () => void;
|
||||
stage: 'counting' | 'categorizing';
|
||||
}
|
||||
|
||||
export const LogCategoriesLoadingContent: React.FC<LogCategoriesLoadingContentProps> = ({
|
||||
onCancel,
|
||||
stage,
|
||||
}) => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
icon={<EuiLoadingSpinner size="xl" />}
|
||||
title={
|
||||
<h2>
|
||||
{stage === 'counting'
|
||||
? logCategoriesLoadingStateCountingTitle
|
||||
: logCategoriesLoadingStateCategorizingTitle}
|
||||
</h2>
|
||||
}
|
||||
actions={
|
||||
onCancel != null
|
||||
? [
|
||||
<EuiButton
|
||||
data-test-subj="o11yLogCategoriesLoadingContentButton"
|
||||
onClick={() => {
|
||||
onCancel();
|
||||
}}
|
||||
>
|
||||
{logCategoriesLoadingStateCancelButtonLabel}
|
||||
</EuiButton>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const logCategoriesLoadingStateCountingTitle = i18n.translate(
|
||||
'xpack.observabilityLogsOverview.logCategoriesGrid.loadingStageCountingTitle',
|
||||
{
|
||||
defaultMessage: 'Estimating log volume',
|
||||
}
|
||||
);
|
||||
|
||||
const logCategoriesLoadingStateCategorizingTitle = i18n.translate(
|
||||
'xpack.observabilityLogsOverview.logCategoriesGrid.loadingStageCategorizingTitle',
|
||||
{
|
||||
defaultMessage: 'Categorizing logs',
|
||||
}
|
||||
);
|
||||
|
||||
const logCategoriesLoadingStateCancelButtonLabel = i18n.translate(
|
||||
'xpack.observabilityLogsOverview.logCategoriesGrid.loadingStateCancelButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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/types';
|
||||
import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { LogCategory } from '../../types';
|
||||
import { IndexNameLogsSourceConfiguration } from '../../utils/logs_source';
|
||||
import {
|
||||
LogCategoriesControlBar,
|
||||
LogCategoriesControlBarDependencies,
|
||||
} from './log_categories_control_bar';
|
||||
import { LogCategoriesGrid, LogCategoriesGridDependencies } from './log_categories_grid';
|
||||
|
||||
export interface LogCategoriesResultContentProps {
|
||||
dependencies: LogCategoriesResultContentDependencies;
|
||||
documentFilters?: QueryDslQueryContainer[];
|
||||
logCategories: LogCategory[];
|
||||
logsSource: IndexNameLogsSourceConfiguration;
|
||||
timeRange: {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type LogCategoriesResultContentDependencies = LogCategoriesControlBarDependencies &
|
||||
LogCategoriesGridDependencies;
|
||||
|
||||
export const LogCategoriesResultContent: React.FC<LogCategoriesResultContentProps> = ({
|
||||
dependencies,
|
||||
documentFilters,
|
||||
logCategories,
|
||||
logsSource,
|
||||
timeRange,
|
||||
}) => {
|
||||
if (logCategories.length === 0) {
|
||||
return <LogCategoriesEmptyResultContent />;
|
||||
} else {
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<LogCategoriesControlBar
|
||||
dependencies={dependencies}
|
||||
documentFilters={documentFilters}
|
||||
logsSource={logsSource}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow>
|
||||
<LogCategoriesGrid dependencies={dependencies} logCategories={logCategories} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const LogCategoriesEmptyResultContent: React.FC = () => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
body={<p>{emptyResultContentDescription}</p>}
|
||||
color="subdued"
|
||||
layout="horizontal"
|
||||
title={<h2>{emptyResultContentTitle}</h2>}
|
||||
titleSize="m"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const emptyResultContentTitle = i18n.translate(
|
||||
'xpack.observabilityLogsOverview.logCategories.emptyResultContentTitle',
|
||||
{
|
||||
defaultMessage: 'No log categories found',
|
||||
}
|
||||
);
|
||||
|
||||
const emptyResultContentDescription = i18n.translate(
|
||||
'xpack.observabilityLogsOverview.logCategories.emptyResultContentDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'No suitable documents within the time range. Try searching for a longer time period.',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 * from './logs_overview';
|
||||
export * from './logs_overview_error_content';
|
||||
export * from './logs_overview_loading_content';
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { type LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public';
|
||||
import React from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { LogsSourceConfiguration, normalizeLogsSource } from '../../utils/logs_source';
|
||||
import { LogCategories, LogCategoriesDependencies } from '../log_categories';
|
||||
import { LogsOverviewErrorContent } from './logs_overview_error_content';
|
||||
import { LogsOverviewLoadingContent } from './logs_overview_loading_content';
|
||||
|
||||
export interface LogsOverviewProps {
|
||||
dependencies: LogsOverviewDependencies;
|
||||
documentFilters?: QueryDslQueryContainer[];
|
||||
logsSource?: LogsSourceConfiguration;
|
||||
timeRange: {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type LogsOverviewDependencies = LogCategoriesDependencies & {
|
||||
logsDataAccess: LogsDataAccessPluginStart;
|
||||
};
|
||||
|
||||
export const LogsOverview: React.FC<LogsOverviewProps> = React.memo(
|
||||
({
|
||||
dependencies,
|
||||
documentFilters = defaultDocumentFilters,
|
||||
logsSource = defaultLogsSource,
|
||||
timeRange,
|
||||
}) => {
|
||||
const normalizedLogsSource = useAsync(
|
||||
() => normalizeLogsSource({ logsDataAccess: dependencies.logsDataAccess })(logsSource),
|
||||
[dependencies.logsDataAccess, logsSource]
|
||||
);
|
||||
|
||||
if (normalizedLogsSource.loading) {
|
||||
return <LogsOverviewLoadingContent />;
|
||||
}
|
||||
|
||||
if (normalizedLogsSource.error != null || normalizedLogsSource.value == null) {
|
||||
return <LogsOverviewErrorContent error={normalizedLogsSource.error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<LogCategories
|
||||
dependencies={dependencies}
|
||||
documentFilters={documentFilters}
|
||||
logsSource={normalizedLogsSource.value}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const defaultDocumentFilters: QueryDslQueryContainer[] = [];
|
||||
|
||||
const defaultLogsSource: LogsSourceConfiguration = { type: 'shared_setting' };
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
export interface LogsOverviewErrorContentProps {
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export const LogsOverviewErrorContent: React.FC<LogsOverviewErrorContentProps> = ({ error }) => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
color="danger"
|
||||
iconType="error"
|
||||
title={<h2>{logsOverviewErrorTitle}</h2>}
|
||||
body={
|
||||
<EuiCodeBlock className="eui-textLeft" whiteSpace="pre">
|
||||
<p>{error?.stack ?? error?.toString() ?? unknownErrorDescription}</p>
|
||||
</EuiCodeBlock>
|
||||
}
|
||||
layout="vertical"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const logsOverviewErrorTitle = i18n.translate('xpack.observabilityLogsOverview.errorTitle', {
|
||||
defaultMessage: 'Error',
|
||||
});
|
||||
|
||||
const unknownErrorDescription = i18n.translate(
|
||||
'xpack.observabilityLogsOverview.unknownErrorDescription',
|
||||
{
|
||||
defaultMessage: 'An unspecified error occurred.',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
export const LogsOverviewLoadingContent: React.FC = ({}) => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
icon={<EuiLoadingSpinner size="xl" />}
|
||||
title={<h2>{logsOverviewLoadingTitle}</h2>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const logsOverviewLoadingTitle = i18n.translate('xpack.observabilityLogsOverview.loadingTitle', {
|
||||
defaultMessage: 'Loading',
|
||||
});
|
|
@ -0,0 +1,282 @@
|
|||
/*
|
||||
* 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 { ISearchGeneric } from '@kbn/search-types';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { fromPromise } from 'xstate5';
|
||||
import { createRandomSamplerWrapper } from '@kbn/ml-random-sampler-utils';
|
||||
import { z } from '@kbn/zod';
|
||||
import { LogCategorizationParams } from './types';
|
||||
import { createCategorizationRequestParams } from './queries';
|
||||
import { LogCategory, LogCategoryChange } from '../../types';
|
||||
|
||||
// the fraction of a category's histogram below which the category is considered rare
|
||||
const rarityThreshold = 0.2;
|
||||
const maxCategoriesCount = 1000;
|
||||
|
||||
export const categorizeDocuments = ({ search }: { search: ISearchGeneric }) =>
|
||||
fromPromise<
|
||||
{
|
||||
categories: LogCategory[];
|
||||
hasReachedLimit: boolean;
|
||||
},
|
||||
LogCategorizationParams & {
|
||||
samplingProbability: number;
|
||||
ignoredCategoryTerms: string[];
|
||||
minDocsPerCategory: number;
|
||||
}
|
||||
>(
|
||||
async ({
|
||||
input: {
|
||||
index,
|
||||
endTimestamp,
|
||||
startTimestamp,
|
||||
timeField,
|
||||
messageField,
|
||||
samplingProbability,
|
||||
ignoredCategoryTerms,
|
||||
documentFilters = [],
|
||||
minDocsPerCategory,
|
||||
},
|
||||
signal,
|
||||
}) => {
|
||||
const randomSampler = createRandomSamplerWrapper({
|
||||
probability: samplingProbability,
|
||||
seed: 1,
|
||||
});
|
||||
|
||||
const requestParams = createCategorizationRequestParams({
|
||||
index,
|
||||
timeField,
|
||||
messageField,
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
randomSampler,
|
||||
additionalFilters: documentFilters,
|
||||
ignoredCategoryTerms,
|
||||
minDocsPerCategory,
|
||||
maxCategoriesCount,
|
||||
});
|
||||
|
||||
const { rawResponse } = await lastValueFrom(
|
||||
search({ params: requestParams }, { abortSignal: signal })
|
||||
);
|
||||
|
||||
if (rawResponse.aggregations == null) {
|
||||
throw new Error('No aggregations found in large categories response');
|
||||
}
|
||||
|
||||
const logCategoriesAggResult = randomSampler.unwrap(rawResponse.aggregations);
|
||||
|
||||
if (!('categories' in logCategoriesAggResult)) {
|
||||
throw new Error('No categorization aggregation found in large categories response');
|
||||
}
|
||||
|
||||
const logCategories =
|
||||
(logCategoriesAggResult.categories.buckets as unknown[]).map(mapCategoryBucket) ?? [];
|
||||
|
||||
return {
|
||||
categories: logCategories,
|
||||
hasReachedLimit: logCategories.length >= maxCategoriesCount,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const mapCategoryBucket = (bucket: any): LogCategory =>
|
||||
esCategoryBucketSchema
|
||||
.transform((parsedBucket) => ({
|
||||
change: mapChangePoint(parsedBucket),
|
||||
documentCount: parsedBucket.doc_count,
|
||||
histogram: parsedBucket.histogram,
|
||||
terms: parsedBucket.key,
|
||||
}))
|
||||
.parse(bucket);
|
||||
|
||||
const mapChangePoint = ({ change, histogram }: EsCategoryBucket): LogCategoryChange => {
|
||||
switch (change.type) {
|
||||
case 'stationary':
|
||||
if (isRareInHistogram(histogram)) {
|
||||
return {
|
||||
type: 'rare',
|
||||
timestamp: findFirstNonZeroBucket(histogram)?.timestamp ?? histogram[0].timestamp,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'none',
|
||||
};
|
||||
}
|
||||
case 'dip':
|
||||
case 'spike':
|
||||
return {
|
||||
type: change.type,
|
||||
timestamp: change.bucket.key,
|
||||
};
|
||||
case 'step_change':
|
||||
return {
|
||||
type: 'step',
|
||||
timestamp: change.bucket.key,
|
||||
};
|
||||
case 'distribution_change':
|
||||
return {
|
||||
type: 'distribution',
|
||||
timestamp: change.bucket.key,
|
||||
};
|
||||
case 'trend_change':
|
||||
return {
|
||||
type: 'trend',
|
||||
timestamp: change.bucket.key,
|
||||
correlationCoefficient: change.details.r_value,
|
||||
};
|
||||
case 'unknown':
|
||||
return {
|
||||
type: 'unknown',
|
||||
rawChange: change.rawChange,
|
||||
};
|
||||
case 'non_stationary':
|
||||
default:
|
||||
return {
|
||||
type: 'other',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The official types are lacking the change_point aggregation
|
||||
*/
|
||||
const esChangePointBucketSchema = z.object({
|
||||
key: z.string().datetime(),
|
||||
doc_count: z.number(),
|
||||
});
|
||||
|
||||
const esChangePointDetailsSchema = z.object({
|
||||
p_value: z.number(),
|
||||
});
|
||||
|
||||
const esChangePointCorrelationSchema = esChangePointDetailsSchema.extend({
|
||||
r_value: z.number(),
|
||||
});
|
||||
|
||||
const esChangePointSchema = z.union([
|
||||
z
|
||||
.object({
|
||||
bucket: esChangePointBucketSchema,
|
||||
type: z.object({
|
||||
dip: esChangePointDetailsSchema,
|
||||
}),
|
||||
})
|
||||
.transform(({ bucket, type: { dip: details } }) => ({
|
||||
type: 'dip' as const,
|
||||
bucket,
|
||||
details,
|
||||
})),
|
||||
z
|
||||
.object({
|
||||
bucket: esChangePointBucketSchema,
|
||||
type: z.object({
|
||||
spike: esChangePointDetailsSchema,
|
||||
}),
|
||||
})
|
||||
.transform(({ bucket, type: { spike: details } }) => ({
|
||||
type: 'spike' as const,
|
||||
bucket,
|
||||
details,
|
||||
})),
|
||||
z
|
||||
.object({
|
||||
bucket: esChangePointBucketSchema,
|
||||
type: z.object({
|
||||
step_change: esChangePointDetailsSchema,
|
||||
}),
|
||||
})
|
||||
.transform(({ bucket, type: { step_change: details } }) => ({
|
||||
type: 'step_change' as const,
|
||||
bucket,
|
||||
details,
|
||||
})),
|
||||
z
|
||||
.object({
|
||||
bucket: esChangePointBucketSchema,
|
||||
type: z.object({
|
||||
trend_change: esChangePointCorrelationSchema,
|
||||
}),
|
||||
})
|
||||
.transform(({ bucket, type: { trend_change: details } }) => ({
|
||||
type: 'trend_change' as const,
|
||||
bucket,
|
||||
details,
|
||||
})),
|
||||
z
|
||||
.object({
|
||||
bucket: esChangePointBucketSchema,
|
||||
type: z.object({
|
||||
distribution_change: esChangePointDetailsSchema,
|
||||
}),
|
||||
})
|
||||
.transform(({ bucket, type: { distribution_change: details } }) => ({
|
||||
type: 'distribution_change' as const,
|
||||
bucket,
|
||||
details,
|
||||
})),
|
||||
z
|
||||
.object({
|
||||
type: z.object({
|
||||
non_stationary: esChangePointCorrelationSchema.extend({
|
||||
trend: z.enum(['increasing', 'decreasing']),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.transform(({ type: { non_stationary: details } }) => ({
|
||||
type: 'non_stationary' as const,
|
||||
details,
|
||||
})),
|
||||
z
|
||||
.object({
|
||||
type: z.object({
|
||||
stationary: z.object({}),
|
||||
}),
|
||||
})
|
||||
.transform(() => ({ type: 'stationary' as const })),
|
||||
z
|
||||
.object({
|
||||
type: z.object({}),
|
||||
})
|
||||
.transform((value) => ({ type: 'unknown' as const, rawChange: JSON.stringify(value) })),
|
||||
]);
|
||||
|
||||
const esHistogramSchema = z
|
||||
.object({
|
||||
buckets: z.array(
|
||||
z
|
||||
.object({
|
||||
key_as_string: z.string(),
|
||||
doc_count: z.number(),
|
||||
})
|
||||
.transform((bucket) => ({
|
||||
timestamp: bucket.key_as_string,
|
||||
documentCount: bucket.doc_count,
|
||||
}))
|
||||
),
|
||||
})
|
||||
.transform(({ buckets }) => buckets);
|
||||
|
||||
type EsHistogram = z.output<typeof esHistogramSchema>;
|
||||
|
||||
const esCategoryBucketSchema = z.object({
|
||||
key: z.string(),
|
||||
doc_count: z.number(),
|
||||
change: esChangePointSchema,
|
||||
histogram: esHistogramSchema,
|
||||
});
|
||||
|
||||
type EsCategoryBucket = z.output<typeof esCategoryBucketSchema>;
|
||||
|
||||
const isRareInHistogram = (histogram: EsHistogram): boolean =>
|
||||
histogram.filter((bucket) => bucket.documentCount > 0).length <
|
||||
histogram.length * rarityThreshold;
|
||||
|
||||
const findFirstNonZeroBucket = (histogram: EsHistogram) =>
|
||||
histogram.find((bucket) => bucket.documentCount > 0);
|
|
@ -0,0 +1,250 @@
|
|||
/*
|
||||
* 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 { MachineImplementationsFrom, assign, setup } from 'xstate5';
|
||||
import { LogCategory } from '../../types';
|
||||
import { getPlaceholderFor } from '../../utils/xstate5_utils';
|
||||
import { categorizeDocuments } from './categorize_documents';
|
||||
import { countDocuments } from './count_documents';
|
||||
import { CategorizeLogsServiceDependencies, LogCategorizationParams } from './types';
|
||||
|
||||
export const categorizeLogsService = setup({
|
||||
types: {
|
||||
input: {} as LogCategorizationParams,
|
||||
output: {} as {
|
||||
categories: LogCategory[];
|
||||
documentCount: number;
|
||||
hasReachedLimit: boolean;
|
||||
samplingProbability: number;
|
||||
},
|
||||
context: {} as {
|
||||
categories: LogCategory[];
|
||||
documentCount: number;
|
||||
error?: Error;
|
||||
hasReachedLimit: boolean;
|
||||
parameters: LogCategorizationParams;
|
||||
samplingProbability: number;
|
||||
},
|
||||
events: {} as {
|
||||
type: 'cancel';
|
||||
},
|
||||
},
|
||||
actors: {
|
||||
countDocuments: getPlaceholderFor(countDocuments),
|
||||
categorizeDocuments: getPlaceholderFor(categorizeDocuments),
|
||||
},
|
||||
actions: {
|
||||
storeError: assign((_, params: { error: unknown }) => ({
|
||||
error: params.error instanceof Error ? params.error : new Error(String(params.error)),
|
||||
})),
|
||||
storeCategories: assign(
|
||||
({ context }, params: { categories: LogCategory[]; hasReachedLimit: boolean }) => ({
|
||||
categories: [...context.categories, ...params.categories],
|
||||
hasReachedLimit: params.hasReachedLimit,
|
||||
})
|
||||
),
|
||||
storeDocumentCount: assign(
|
||||
(_, params: { documentCount: number; samplingProbability: number }) => ({
|
||||
documentCount: params.documentCount,
|
||||
samplingProbability: params.samplingProbability,
|
||||
})
|
||||
),
|
||||
},
|
||||
guards: {
|
||||
hasTooFewDocuments: (_guardArgs, params: { documentCount: number }) => params.documentCount < 1,
|
||||
requiresSampling: (_guardArgs, params: { samplingProbability: number }) =>
|
||||
params.samplingProbability < 1,
|
||||
},
|
||||
}).createMachine({
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QGMCGAXMUD2AnAlgF5gAy2UsAdMtgK4B26+9UAItsrQLZiOwDEEbPTCVmAN2wBrUWkw4CxMhWp1GzNh2690sBBI4Z8wgNoAGALrmLiUAAdssfE2G2QAD0QBmMwA5KACy+AQFmob4AjABMwQBsADQgAJ6IkYEAnJkA7FmxZlERmQGxAL4liXJYeESk5FQ0DEws7Jw8fILCogYy1BhVirUqDerNWm26+vSScsb01iYRNkggDk4u9G6eCD7+QSFhftFxiSkIvgCsWZSxEVlRsbFZ52Zm515lFX0KNcr1ak2aVo6ARCERiKbSWRfapKOqqRoaFraPiTaZGUyWExRJb2RzOWabbx+QLBULhI7FE7eWL+F45GnRPIRZkfECVb6wob-RFjYH8MC4XB4Sh2AA2GAAZnguL15DDBn8EaMgSiDDMMVZLG5VvjXMstjsSftyTFKclEOdzgFKF5zukvA8zBFnl50udWez5b94SNAcjdPw0PRkGBRdZtXj1oTtsS9mTDqaEuaEBF8udKFkIr5fK6olkzOksgEPdCBt6JWB0MgABYaADKqC4YsgAGFS-g4B0wd0oXKBg2m6LW+24OHljqo-rEMzbpQos8-K7fC9CknTrF0rEbbb0oVMoWIgF3eU2e3OVQK1XaywB82IG2+x2BAKhbgReL0FLcDLPf3G3eH36J8x1xNYCSnFNmSuecXhzdJlydTcqQQLJfHSOc0PyLJN3SMxYiPEtH3PShLxret-yHe8RwEIMQzDLVx0jcDQC2GdoIXOCENXZDsyiOcAiiKJ0iiPDLi8V1CKA4jSOvKAACUwC4VBmA0QDvk7UEughHpfxqBSlJUlg1OqUcGNA3UNggrMs347IjzdaIvGQwSvECXI8k3Z43gEiJJI5BUSMrMiWH05T6FU6j+UFYUxUlaVZSksBQsMqBjIIUycRWJi9RY6dIn8KIAjsu1zkc5CAmiG1fBiaIzB8B0QmPT4iICmSNGS8KjMi2jQxArKwJyjw8pswriocqInOTLwIi3ASD1yQpswCd5WXobAIDgNxdPPCMBss3KEAAWjXRBDvTfcLsu9Jlr8r04WGAEkXGeBGL26MBOQzIt2ut4cwmirCt8W6yzhNqbwo4dH0216LOjTMIjnBdYhK1DYgdHjihtZbUIdWIXJuYGflBoLZI6iKoZe8zJwOw9KtGt1kbuTcsmQrwi0oeCQjzZ5blwt1Cek5TKN22GIIKZbAgKC45pyLyeLwtz4Kyabs1QgWAs0kXqaGhBxdcnzpaE2XXmch0MORmaBJeLwjbKMogA */
|
||||
id: 'categorizeLogs',
|
||||
context: ({ input }) => ({
|
||||
categories: [],
|
||||
documentCount: 0,
|
||||
hasReachedLimit: false,
|
||||
parameters: input,
|
||||
samplingProbability: 1,
|
||||
}),
|
||||
initial: 'countingDocuments',
|
||||
states: {
|
||||
countingDocuments: {
|
||||
invoke: {
|
||||
src: 'countDocuments',
|
||||
input: ({ context }) => context.parameters,
|
||||
onDone: [
|
||||
{
|
||||
target: 'done',
|
||||
guard: {
|
||||
type: 'hasTooFewDocuments',
|
||||
params: ({ event }) => event.output,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
type: 'storeDocumentCount',
|
||||
params: ({ event }) => event.output,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
target: 'fetchingSampledCategories',
|
||||
guard: {
|
||||
type: 'requiresSampling',
|
||||
params: ({ event }) => event.output,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
type: 'storeDocumentCount',
|
||||
params: ({ event }) => event.output,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
target: 'fetchingRemainingCategories',
|
||||
actions: [
|
||||
{
|
||||
type: 'storeDocumentCount',
|
||||
params: ({ event }) => event.output,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
target: 'failed',
|
||||
actions: [
|
||||
{
|
||||
type: 'storeError',
|
||||
params: ({ event }) => ({ error: event.error }),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
on: {
|
||||
cancel: {
|
||||
target: 'failed',
|
||||
actions: [
|
||||
{
|
||||
type: 'storeError',
|
||||
params: () => ({ error: new Error('Counting cancelled') }),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
fetchingSampledCategories: {
|
||||
invoke: {
|
||||
src: 'categorizeDocuments',
|
||||
id: 'categorizeSampledCategories',
|
||||
input: ({ context }) => ({
|
||||
...context.parameters,
|
||||
samplingProbability: context.samplingProbability,
|
||||
ignoredCategoryTerms: [],
|
||||
minDocsPerCategory: 10,
|
||||
}),
|
||||
onDone: {
|
||||
target: 'fetchingRemainingCategories',
|
||||
actions: [
|
||||
{
|
||||
type: 'storeCategories',
|
||||
params: ({ event }) => event.output,
|
||||
},
|
||||
],
|
||||
},
|
||||
onError: {
|
||||
target: 'failed',
|
||||
actions: [
|
||||
{
|
||||
type: 'storeError',
|
||||
params: ({ event }) => ({ error: event.error }),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
on: {
|
||||
cancel: {
|
||||
target: 'failed',
|
||||
actions: [
|
||||
{
|
||||
type: 'storeError',
|
||||
params: () => ({ error: new Error('Categorization cancelled') }),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
fetchingRemainingCategories: {
|
||||
invoke: {
|
||||
src: 'categorizeDocuments',
|
||||
id: 'categorizeRemainingCategories',
|
||||
input: ({ context }) => ({
|
||||
...context.parameters,
|
||||
samplingProbability: 1,
|
||||
ignoredCategoryTerms: context.categories.map((category) => category.terms),
|
||||
minDocsPerCategory: 0,
|
||||
}),
|
||||
onDone: {
|
||||
target: 'done',
|
||||
actions: [
|
||||
{
|
||||
type: 'storeCategories',
|
||||
params: ({ event }) => event.output,
|
||||
},
|
||||
],
|
||||
},
|
||||
onError: {
|
||||
target: 'failed',
|
||||
actions: [
|
||||
{
|
||||
type: 'storeError',
|
||||
params: ({ event }) => ({ error: event.error }),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
on: {
|
||||
cancel: {
|
||||
target: 'failed',
|
||||
actions: [
|
||||
{
|
||||
type: 'storeError',
|
||||
params: () => ({ error: new Error('Categorization cancelled') }),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
failed: {
|
||||
type: 'final',
|
||||
},
|
||||
|
||||
done: {
|
||||
type: 'final',
|
||||
},
|
||||
},
|
||||
output: ({ context }) => ({
|
||||
categories: context.categories,
|
||||
documentCount: context.documentCount,
|
||||
hasReachedLimit: context.hasReachedLimit,
|
||||
samplingProbability: context.samplingProbability,
|
||||
}),
|
||||
});
|
||||
|
||||
export const createCategorizeLogsServiceImplementations = ({
|
||||
search,
|
||||
}: CategorizeLogsServiceDependencies): MachineImplementationsFrom<
|
||||
typeof categorizeLogsService
|
||||
> => ({
|
||||
actors: {
|
||||
categorizeDocuments: categorizeDocuments({ search }),
|
||||
countDocuments: countDocuments({ search }),
|
||||
},
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 { getSampleProbability } from '@kbn/ml-random-sampler-utils';
|
||||
import { ISearchGeneric } from '@kbn/search-types';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { fromPromise } from 'xstate5';
|
||||
import { LogCategorizationParams } from './types';
|
||||
import { createCategorizationQuery } from './queries';
|
||||
|
||||
export const countDocuments = ({ search }: { search: ISearchGeneric }) =>
|
||||
fromPromise<
|
||||
{
|
||||
documentCount: number;
|
||||
samplingProbability: number;
|
||||
},
|
||||
LogCategorizationParams
|
||||
>(
|
||||
async ({
|
||||
input: { index, endTimestamp, startTimestamp, timeField, messageField, documentFilters },
|
||||
signal,
|
||||
}) => {
|
||||
const { rawResponse: totalHitsResponse } = await lastValueFrom(
|
||||
search(
|
||||
{
|
||||
params: {
|
||||
index,
|
||||
size: 0,
|
||||
track_total_hits: true,
|
||||
query: createCategorizationQuery({
|
||||
messageField,
|
||||
timeField,
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
additionalFilters: documentFilters,
|
||||
}),
|
||||
},
|
||||
},
|
||||
{ abortSignal: signal }
|
||||
)
|
||||
);
|
||||
|
||||
const documentCount =
|
||||
totalHitsResponse.hits.total == null
|
||||
? 0
|
||||
: typeof totalHitsResponse.hits.total === 'number'
|
||||
? totalHitsResponse.hits.total
|
||||
: totalHitsResponse.hits.total.value;
|
||||
const samplingProbability = getSampleProbability(documentCount);
|
||||
|
||||
return {
|
||||
documentCount,
|
||||
samplingProbability,
|
||||
};
|
||||
}
|
||||
);
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 * from './categorize_logs_service';
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { calculateAuto } from '@kbn/calculate-auto';
|
||||
import { RandomSamplerWrapper } from '@kbn/ml-random-sampler-utils';
|
||||
import moment from 'moment';
|
||||
|
||||
const isoTimestampFormat = "YYYY-MM-DD'T'HH:mm:ss.SSS'Z'";
|
||||
|
||||
export const createCategorizationQuery = ({
|
||||
messageField,
|
||||
timeField,
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
additionalFilters = [],
|
||||
ignoredCategoryTerms = [],
|
||||
}: {
|
||||
messageField: string;
|
||||
timeField: string;
|
||||
startTimestamp: string;
|
||||
endTimestamp: string;
|
||||
additionalFilters?: QueryDslQueryContainer[];
|
||||
ignoredCategoryTerms?: string[];
|
||||
}): QueryDslQueryContainer => {
|
||||
return {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
exists: {
|
||||
field: messageField,
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
[timeField]: {
|
||||
gte: startTimestamp,
|
||||
lte: endTimestamp,
|
||||
format: 'strict_date_time',
|
||||
},
|
||||
},
|
||||
},
|
||||
...additionalFilters,
|
||||
],
|
||||
must_not: ignoredCategoryTerms.map(createCategoryQuery(messageField)),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const createCategorizationRequestParams = ({
|
||||
index,
|
||||
timeField,
|
||||
messageField,
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
randomSampler,
|
||||
minDocsPerCategory = 0,
|
||||
additionalFilters = [],
|
||||
ignoredCategoryTerms = [],
|
||||
maxCategoriesCount = 1000,
|
||||
}: {
|
||||
startTimestamp: string;
|
||||
endTimestamp: string;
|
||||
index: string;
|
||||
timeField: string;
|
||||
messageField: string;
|
||||
randomSampler: RandomSamplerWrapper;
|
||||
minDocsPerCategory?: number;
|
||||
additionalFilters?: QueryDslQueryContainer[];
|
||||
ignoredCategoryTerms?: string[];
|
||||
maxCategoriesCount?: number;
|
||||
}) => {
|
||||
const startMoment = moment(startTimestamp, isoTimestampFormat);
|
||||
const endMoment = moment(endTimestamp, isoTimestampFormat);
|
||||
const fixedIntervalDuration = calculateAuto.atLeast(
|
||||
24,
|
||||
moment.duration(endMoment.diff(startMoment))
|
||||
);
|
||||
const fixedIntervalSize = `${Math.ceil(fixedIntervalDuration?.asMinutes() ?? 1)}m`;
|
||||
|
||||
return {
|
||||
index,
|
||||
size: 0,
|
||||
track_total_hits: false,
|
||||
query: createCategorizationQuery({
|
||||
messageField,
|
||||
timeField,
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
additionalFilters,
|
||||
ignoredCategoryTerms,
|
||||
}),
|
||||
aggs: randomSampler.wrap({
|
||||
histogram: {
|
||||
date_histogram: {
|
||||
field: timeField,
|
||||
fixed_interval: fixedIntervalSize,
|
||||
extended_bounds: {
|
||||
min: startTimestamp,
|
||||
max: endTimestamp,
|
||||
},
|
||||
},
|
||||
},
|
||||
categories: {
|
||||
categorize_text: {
|
||||
field: messageField,
|
||||
size: maxCategoriesCount,
|
||||
categorization_analyzer: {
|
||||
tokenizer: 'standard',
|
||||
},
|
||||
...(minDocsPerCategory > 0 ? { min_doc_count: minDocsPerCategory } : {}),
|
||||
},
|
||||
aggs: {
|
||||
histogram: {
|
||||
date_histogram: {
|
||||
field: timeField,
|
||||
fixed_interval: fixedIntervalSize,
|
||||
extended_bounds: {
|
||||
min: startTimestamp,
|
||||
max: endTimestamp,
|
||||
},
|
||||
},
|
||||
},
|
||||
change: {
|
||||
// @ts-expect-error the official types don't support the change_point aggregation
|
||||
change_point: {
|
||||
buckets_path: 'histogram>_count',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export const createCategoryQuery =
|
||||
(messageField: string) =>
|
||||
(categoryTerms: string): QueryDslQueryContainer => ({
|
||||
match: {
|
||||
[messageField]: {
|
||||
query: categoryTerms,
|
||||
operator: 'AND' as const,
|
||||
fuzziness: 0,
|
||||
auto_generate_synonyms_phrase_query: false,
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { ISearchGeneric } from '@kbn/search-types';
|
||||
|
||||
export interface CategorizeLogsServiceDependencies {
|
||||
search: ISearchGeneric;
|
||||
}
|
||||
|
||||
export interface LogCategorizationParams {
|
||||
documentFilters: QueryDslQueryContainer[];
|
||||
endTimestamp: string;
|
||||
index: string;
|
||||
messageField: string;
|
||||
startTimestamp: string;
|
||||
timeField: string;
|
||||
}
|
74
x-pack/packages/observability/logs_overview/src/types.ts
Normal file
74
x-pack/packages/observability/logs_overview/src/types.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export interface LogCategory {
|
||||
change: LogCategoryChange;
|
||||
documentCount: number;
|
||||
histogram: LogCategoryHistogramBucket[];
|
||||
terms: string;
|
||||
}
|
||||
|
||||
export type LogCategoryChange =
|
||||
| LogCategoryNoChange
|
||||
| LogCategoryRareChange
|
||||
| LogCategorySpikeChange
|
||||
| LogCategoryDipChange
|
||||
| LogCategoryStepChange
|
||||
| LogCategoryDistributionChange
|
||||
| LogCategoryTrendChange
|
||||
| LogCategoryOtherChange
|
||||
| LogCategoryUnknownChange;
|
||||
|
||||
export interface LogCategoryNoChange {
|
||||
type: 'none';
|
||||
}
|
||||
|
||||
export interface LogCategoryRareChange {
|
||||
type: 'rare';
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface LogCategorySpikeChange {
|
||||
type: 'spike';
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface LogCategoryDipChange {
|
||||
type: 'dip';
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface LogCategoryStepChange {
|
||||
type: 'step';
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface LogCategoryTrendChange {
|
||||
type: 'trend';
|
||||
timestamp: string;
|
||||
correlationCoefficient: number;
|
||||
}
|
||||
|
||||
export interface LogCategoryDistributionChange {
|
||||
type: 'distribution';
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface LogCategoryOtherChange {
|
||||
type: 'other';
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
export interface LogCategoryUnknownChange {
|
||||
type: 'unknown';
|
||||
rawChange: string;
|
||||
}
|
||||
|
||||
export interface LogCategoryHistogramBucket {
|
||||
documentCount: number;
|
||||
timestamp: string;
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 AbstractDataView } from '@kbn/data-views-plugin/common';
|
||||
import { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public';
|
||||
|
||||
export type LogsSourceConfiguration =
|
||||
| SharedSettingLogsSourceConfiguration
|
||||
| IndexNameLogsSourceConfiguration
|
||||
| DataViewLogsSourceConfiguration;
|
||||
|
||||
export interface SharedSettingLogsSourceConfiguration {
|
||||
type: 'shared_setting';
|
||||
timestampField?: string;
|
||||
messageField?: string;
|
||||
}
|
||||
|
||||
export interface IndexNameLogsSourceConfiguration {
|
||||
type: 'index_name';
|
||||
indexName: string;
|
||||
timestampField: string;
|
||||
messageField: string;
|
||||
}
|
||||
|
||||
export interface DataViewLogsSourceConfiguration {
|
||||
type: 'data_view';
|
||||
dataView: AbstractDataView;
|
||||
messageField?: string;
|
||||
}
|
||||
|
||||
export const normalizeLogsSource =
|
||||
({ logsDataAccess }: { logsDataAccess: LogsDataAccessPluginStart }) =>
|
||||
async (logsSource: LogsSourceConfiguration): Promise<IndexNameLogsSourceConfiguration> => {
|
||||
switch (logsSource.type) {
|
||||
case 'index_name':
|
||||
return logsSource;
|
||||
case 'shared_setting':
|
||||
const logSourcesFromSharedSettings =
|
||||
await logsDataAccess.services.logSourcesService.getLogSources();
|
||||
return {
|
||||
type: 'index_name',
|
||||
indexName: logSourcesFromSharedSettings
|
||||
.map((logSource) => logSource.indexPattern)
|
||||
.join(','),
|
||||
timestampField: logsSource.timestampField ?? '@timestamp',
|
||||
messageField: logsSource.messageField ?? 'message',
|
||||
};
|
||||
case 'data_view':
|
||||
return {
|
||||
type: 'index_name',
|
||||
indexName: logsSource.dataView.getIndexPattern(),
|
||||
timestampField: logsSource.dataView.timeFieldName ?? '@timestamp',
|
||||
messageField: logsSource.messageField ?? 'message',
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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 const getPlaceholderFor = <ImplementationFactory extends (...factoryArgs: any[]) => any>(
|
||||
implementationFactory: ImplementationFactory
|
||||
): ReturnType<ImplementationFactory> =>
|
||||
(() => {
|
||||
throw new Error('Not implemented');
|
||||
}) as ReturnType<ImplementationFactory>;
|
39
x-pack/packages/observability/logs_overview/tsconfig.json
Normal file
39
x-pack/packages/observability/logs_overview/tsconfig.json
Normal file
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react",
|
||||
"@kbn/ambient-ui-types",
|
||||
"@kbn/ambient-storybook-types",
|
||||
"@emotion/react/types/css-prop"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/i18n",
|
||||
"@kbn/search-types",
|
||||
"@kbn/xstate-utils",
|
||||
"@kbn/core-ui-settings-browser",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/charts-plugin",
|
||||
"@kbn/utility-types",
|
||||
"@kbn/logs-data-access-plugin",
|
||||
"@kbn/ml-random-sampler-utils",
|
||||
"@kbn/zod",
|
||||
"@kbn/calculate-auto",
|
||||
"@kbn/discover-plugin",
|
||||
"@kbn/es-query",
|
||||
"@kbn/router-utils",
|
||||
"@kbn/share-plugin",
|
||||
]
|
||||
}
|
|
@ -5,19 +5,36 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import moment from 'moment';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { LogStream } from '@kbn/logs-shared-plugin/public';
|
||||
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||
import { useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { CONTAINER_ID, SERVICE_ENVIRONMENT, SERVICE_NAME } from '../../../../common/es_fields/apm';
|
||||
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
|
||||
import { useKibana } from '../../../context/kibana_context/use_kibana';
|
||||
import { useAnyOfApmParams } from '../../../hooks/use_apm_params';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
import { APIReturnType } from '../../../services/rest/create_call_apm_api';
|
||||
|
||||
import { CONTAINER_ID, SERVICE_ENVIRONMENT, SERVICE_NAME } from '../../../../common/es_fields/apm';
|
||||
import { useAnyOfApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
|
||||
export function ServiceLogs() {
|
||||
const {
|
||||
services: {
|
||||
logsShared: { LogsOverview },
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const isLogsOverviewEnabled = LogsOverview.useIsEnabled();
|
||||
|
||||
if (isLogsOverviewEnabled) {
|
||||
return <ServiceLogsOverview />;
|
||||
} else {
|
||||
return <ClassicServiceLogsStream />;
|
||||
}
|
||||
}
|
||||
|
||||
export function ClassicServiceLogsStream() {
|
||||
const { serviceName } = useApmServiceContext();
|
||||
|
||||
const {
|
||||
|
@ -58,6 +75,54 @@ export function ServiceLogs() {
|
|||
);
|
||||
}
|
||||
|
||||
export function ServiceLogsOverview() {
|
||||
const {
|
||||
services: { logsShared },
|
||||
} = useKibana();
|
||||
const { serviceName } = useApmServiceContext();
|
||||
const {
|
||||
query: { environment, kuery, rangeFrom, rangeTo },
|
||||
} = useAnyOfApmParams('/services/{serviceName}/logs');
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
const timeRange = useMemo(() => ({ start, end }), [start, end]);
|
||||
|
||||
const { data: logFilters, status } = useFetcher(
|
||||
async (callApmApi) => {
|
||||
if (start == null || end == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { containerIds } = await callApmApi(
|
||||
'GET /internal/apm/services/{serviceName}/infrastructure_attributes',
|
||||
{
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return [getInfrastructureFilter({ containerIds, environment, serviceName })];
|
||||
},
|
||||
[environment, kuery, serviceName, start, end]
|
||||
);
|
||||
|
||||
if (status === FETCH_STATUS.SUCCESS) {
|
||||
return <logsShared.LogsOverview documentFilters={logFilters} timeRange={timeRange} />;
|
||||
} else if (status === FETCH_STATUS.FAILURE) {
|
||||
return (
|
||||
<logsShared.LogsOverview.ErrorContent error={new Error('Failed to fetch service details')} />
|
||||
);
|
||||
} else {
|
||||
return <logsShared.LogsOverview.LoadingContent />;
|
||||
}
|
||||
}
|
||||
|
||||
export function getInfrastructureKQLFilter({
|
||||
data,
|
||||
serviceName,
|
||||
|
@ -84,3 +149,99 @@ export function getInfrastructureKQLFilter({
|
|||
|
||||
return [serviceNameAndEnvironmentCorrelation, ...containerIdCorrelation].join(' or ');
|
||||
}
|
||||
|
||||
export function getInfrastructureFilter({
|
||||
containerIds,
|
||||
environment,
|
||||
serviceName,
|
||||
}: {
|
||||
containerIds: string[];
|
||||
environment: string;
|
||||
serviceName: string;
|
||||
}): QueryDslQueryContainer {
|
||||
return {
|
||||
bool: {
|
||||
should: [
|
||||
...getServiceShouldClauses({ environment, serviceName }),
|
||||
...getContainerShouldClauses({ containerIds }),
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getServiceShouldClauses({
|
||||
environment,
|
||||
serviceName,
|
||||
}: {
|
||||
environment: string;
|
||||
serviceName: string;
|
||||
}): QueryDslQueryContainer[] {
|
||||
const serviceNameFilter: QueryDslQueryContainer = {
|
||||
term: {
|
||||
[SERVICE_NAME]: serviceName,
|
||||
},
|
||||
};
|
||||
|
||||
if (environment === ENVIRONMENT_ALL.value) {
|
||||
return [serviceNameFilter];
|
||||
} else {
|
||||
return [
|
||||
{
|
||||
bool: {
|
||||
filter: [
|
||||
serviceNameFilter,
|
||||
{
|
||||
term: {
|
||||
[SERVICE_ENVIRONMENT]: environment,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
filter: [serviceNameFilter],
|
||||
must_not: [
|
||||
{
|
||||
exists: {
|
||||
field: SERVICE_ENVIRONMENT,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export function getContainerShouldClauses({
|
||||
containerIds = [],
|
||||
}: {
|
||||
containerIds: string[];
|
||||
}): QueryDslQueryContainer[] {
|
||||
if (containerIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
terms: {
|
||||
[CONTAINER_ID]: containerIds,
|
||||
},
|
||||
},
|
||||
],
|
||||
must_not: [
|
||||
{
|
||||
term: {
|
||||
[SERVICE_NAME]: '*',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -330,7 +330,7 @@ export const serviceDetailRoute = {
|
|||
}),
|
||||
element: <ServiceLogs />,
|
||||
searchBarOptions: {
|
||||
showUnifiedSearchBar: false,
|
||||
showQueryInput: false,
|
||||
},
|
||||
}),
|
||||
'/services/{serviceName}/infrastructure': {
|
||||
|
|
|
@ -69,6 +69,7 @@ import { from } from 'rxjs';
|
|||
import { map } from 'rxjs';
|
||||
import type { CloudSetup } from '@kbn/cloud-plugin/public';
|
||||
import type { ServerlessPluginStart } from '@kbn/serverless/public';
|
||||
import { LogsSharedClientStartExports } from '@kbn/logs-shared-plugin/public';
|
||||
import type { ConfigSchema } from '.';
|
||||
import { registerApmRuleTypes } from './components/alerting/rule_types/register_apm_rule_types';
|
||||
import { registerEmbeddables } from './embeddable/register_embeddables';
|
||||
|
@ -142,6 +143,7 @@ export interface ApmPluginStartDeps {
|
|||
dashboard: DashboardStart;
|
||||
metricsDataAccess: MetricsDataPluginStart;
|
||||
uiSettings: IUiSettingsClient;
|
||||
logsShared: LogsSharedClientStartExports;
|
||||
}
|
||||
|
||||
const applicationsTitle = i18n.translate('xpack.apm.navigation.rootTitle', {
|
||||
|
|
|
@ -5,21 +5,37 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { LogStream } from '@kbn/logs-shared-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
import { InfraLoadingPanel } from '../../../../../../components/loading';
|
||||
import { useKibanaContextForPlugin } from '../../../../../../hooks/use_kibana';
|
||||
import { useLogViewReference } from '../../../../../../hooks/use_log_view_reference';
|
||||
import { buildCombinedAssetFilter } from '../../../../../../utils/filters/build';
|
||||
import { useHostsViewContext } from '../../../hooks/use_hosts_view';
|
||||
import { useUnifiedSearchContext } from '../../../hooks/use_unified_search';
|
||||
import { useLogsSearchUrlState } from '../../../hooks/use_logs_search_url_state';
|
||||
import { useUnifiedSearchContext } from '../../../hooks/use_unified_search';
|
||||
import { LogsLinkToStream } from './logs_link_to_stream';
|
||||
import { LogsSearchBar } from './logs_search_bar';
|
||||
import { buildCombinedAssetFilter } from '../../../../../../utils/filters/build';
|
||||
import { useLogViewReference } from '../../../../../../hooks/use_log_view_reference';
|
||||
|
||||
export const LogsTabContent = () => {
|
||||
const {
|
||||
services: {
|
||||
logsShared: { LogsOverview },
|
||||
},
|
||||
} = useKibanaContextForPlugin();
|
||||
const isLogsOverviewEnabled = LogsOverview.useIsEnabled();
|
||||
if (isLogsOverviewEnabled) {
|
||||
return <LogsTabLogsOverviewContent />;
|
||||
} else {
|
||||
return <LogsTabLogStreamContent />;
|
||||
}
|
||||
};
|
||||
|
||||
export const LogsTabLogStreamContent = () => {
|
||||
const [filterQuery] = useLogsSearchUrlState();
|
||||
const { getDateRangeAsTimestamp } = useUnifiedSearchContext();
|
||||
const { from, to } = useMemo(() => getDateRangeAsTimestamp(), [getDateRangeAsTimestamp]);
|
||||
|
@ -53,22 +69,7 @@ export const LogsTabContent = () => {
|
|||
}, [filterQuery.query, hostNodes]);
|
||||
|
||||
if (loading || logViewLoading || !logView) {
|
||||
return (
|
||||
<EuiFlexGroup style={{ height: 300 }} direction="column" alignItems="stretch">
|
||||
<EuiFlexItem grow>
|
||||
<InfraLoadingPanel
|
||||
width="100%"
|
||||
height="100%"
|
||||
text={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.hostsViewPage.tabs.logs.loadingEntriesLabel"
|
||||
defaultMessage="Loading entries"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
return <LogsTabLoadingContent />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -112,3 +113,53 @@ const createHostsFilterQueryParam = (hostNodes: string[]): string => {
|
|||
|
||||
return hostsQueryParam;
|
||||
};
|
||||
|
||||
const LogsTabLogsOverviewContent = () => {
|
||||
const {
|
||||
services: {
|
||||
logsShared: { LogsOverview },
|
||||
},
|
||||
} = useKibanaContextForPlugin();
|
||||
|
||||
const { parsedDateRange } = useUnifiedSearchContext();
|
||||
const timeRange = useMemo(
|
||||
() => ({ start: parsedDateRange.from, end: parsedDateRange.to }),
|
||||
[parsedDateRange.from, parsedDateRange.to]
|
||||
);
|
||||
|
||||
const { hostNodes, loading, error } = useHostsViewContext();
|
||||
const logFilters = useMemo(
|
||||
() => [
|
||||
buildCombinedAssetFilter({
|
||||
field: 'host.name',
|
||||
values: hostNodes.map((p) => p.name),
|
||||
}).query as QueryDslQueryContainer,
|
||||
],
|
||||
[hostNodes]
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return <LogsOverview.LoadingContent />;
|
||||
} else if (error != null) {
|
||||
return <LogsOverview.ErrorContent error={error} />;
|
||||
} else {
|
||||
return <LogsOverview documentFilters={logFilters} timeRange={timeRange} />;
|
||||
}
|
||||
};
|
||||
|
||||
const LogsTabLoadingContent = () => (
|
||||
<EuiFlexGroup style={{ height: 300 }} direction="column" alignItems="stretch">
|
||||
<EuiFlexItem grow>
|
||||
<InfraLoadingPanel
|
||||
width="100%"
|
||||
height="100%"
|
||||
text={
|
||||
<FormattedMessage
|
||||
id="xpack.infra.hostsViewPage.tabs.logs.loadingEntriesLabel"
|
||||
defaultMessage="Loading entries"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -9,13 +9,14 @@
|
|||
"browser": true,
|
||||
"configPath": ["xpack", "logs_shared"],
|
||||
"requiredPlugins": [
|
||||
"charts",
|
||||
"data",
|
||||
"dataViews",
|
||||
"discoverShared",
|
||||
"usageCollection",
|
||||
"logsDataAccess",
|
||||
"observabilityShared",
|
||||
"share",
|
||||
"logsDataAccess"
|
||||
"usageCollection",
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"observabilityAIAssistant",
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 * from './logs_overview';
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type {
|
||||
LogsOverviewProps,
|
||||
SelfContainedLogsOverviewComponent,
|
||||
SelfContainedLogsOverviewHelpers,
|
||||
} from './logs_overview';
|
||||
|
||||
export const createLogsOverviewMock = () => {
|
||||
const LogsOverviewMock = jest.fn(LogsOverviewMockImpl) as unknown as ILogsOverviewMock;
|
||||
|
||||
LogsOverviewMock.useIsEnabled = jest.fn(() => true);
|
||||
|
||||
LogsOverviewMock.ErrorContent = jest.fn(() => <div />);
|
||||
|
||||
LogsOverviewMock.LoadingContent = jest.fn(() => <div />);
|
||||
|
||||
return LogsOverviewMock;
|
||||
};
|
||||
|
||||
const LogsOverviewMockImpl = (_props: LogsOverviewProps) => {
|
||||
return <div />;
|
||||
};
|
||||
|
||||
type ILogsOverviewMock = jest.Mocked<SelfContainedLogsOverviewComponent> &
|
||||
jest.Mocked<SelfContainedLogsOverviewHelpers>;
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 { OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID } from '@kbn/management-settings-ids';
|
||||
import type {
|
||||
LogsOverviewProps as FullLogsOverviewProps,
|
||||
LogsOverviewDependencies,
|
||||
LogsOverviewErrorContentProps,
|
||||
} from '@kbn/observability-logs-overview';
|
||||
import { dynamic } from '@kbn/shared-ux-utility';
|
||||
import React from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
|
||||
const LazyLogsOverview = dynamic(() =>
|
||||
import('@kbn/observability-logs-overview').then((mod) => ({ default: mod.LogsOverview }))
|
||||
);
|
||||
|
||||
const LazyLogsOverviewErrorContent = dynamic(() =>
|
||||
import('@kbn/observability-logs-overview').then((mod) => ({
|
||||
default: mod.LogsOverviewErrorContent,
|
||||
}))
|
||||
);
|
||||
|
||||
const LazyLogsOverviewLoadingContent = dynamic(() =>
|
||||
import('@kbn/observability-logs-overview').then((mod) => ({
|
||||
default: mod.LogsOverviewLoadingContent,
|
||||
}))
|
||||
);
|
||||
|
||||
export type LogsOverviewProps = Omit<FullLogsOverviewProps, 'dependencies'>;
|
||||
|
||||
export interface SelfContainedLogsOverviewHelpers {
|
||||
useIsEnabled: () => boolean;
|
||||
ErrorContent: React.ComponentType<LogsOverviewErrorContentProps>;
|
||||
LoadingContent: React.ComponentType;
|
||||
}
|
||||
|
||||
export type SelfContainedLogsOverviewComponent = React.ComponentType<LogsOverviewProps>;
|
||||
|
||||
export type SelfContainedLogsOverview = SelfContainedLogsOverviewComponent &
|
||||
SelfContainedLogsOverviewHelpers;
|
||||
|
||||
export const createLogsOverview = (
|
||||
dependencies: LogsOverviewDependencies
|
||||
): SelfContainedLogsOverview => {
|
||||
const SelfContainedLogsOverview = (props: LogsOverviewProps) => {
|
||||
return <LazyLogsOverview dependencies={dependencies} {...props} />;
|
||||
};
|
||||
|
||||
const isEnabled$ = dependencies.uiSettings.client.get$(
|
||||
OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID,
|
||||
defaultIsEnabled
|
||||
);
|
||||
|
||||
SelfContainedLogsOverview.useIsEnabled = (): boolean => {
|
||||
return useObservable<boolean>(isEnabled$, defaultIsEnabled);
|
||||
};
|
||||
|
||||
SelfContainedLogsOverview.ErrorContent = LazyLogsOverviewErrorContent;
|
||||
|
||||
SelfContainedLogsOverview.LoadingContent = LazyLogsOverviewLoadingContent;
|
||||
|
||||
return SelfContainedLogsOverview;
|
||||
};
|
||||
|
||||
const defaultIsEnabled = false;
|
|
@ -50,6 +50,7 @@ export type {
|
|||
UpdatedDateRange,
|
||||
VisibleInterval,
|
||||
} from './components/logging/log_text_stream/scrollable_log_text_stream_view';
|
||||
export type { LogsOverviewProps } from './components/logs_overview';
|
||||
|
||||
export const WithSummary = dynamic(() => import('./containers/logs/log_summary/with_summary'));
|
||||
export const LogEntryFlyout = dynamic(
|
||||
|
|
|
@ -6,12 +6,14 @@
|
|||
*/
|
||||
|
||||
import { createLogAIAssistantMock } from './components/log_ai_assistant/log_ai_assistant.mock';
|
||||
import { createLogsOverviewMock } from './components/logs_overview/logs_overview.mock';
|
||||
import { createLogViewsServiceStartMock } from './services/log_views/log_views_service.mock';
|
||||
import { LogsSharedClientStartExports } from './types';
|
||||
|
||||
export const createLogsSharedPluginStartMock = (): jest.Mocked<LogsSharedClientStartExports> => ({
|
||||
logViews: createLogViewsServiceStartMock(),
|
||||
LogAIAssistant: createLogAIAssistantMock(),
|
||||
LogsOverview: createLogsOverviewMock(),
|
||||
});
|
||||
|
||||
export const _ensureTypeCompatibility = (): LogsSharedClientStartExports =>
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
TraceLogsLocatorDefinition,
|
||||
} from '../common/locators';
|
||||
import { createLogAIAssistant, createLogsAIAssistantRenderer } from './components/log_ai_assistant';
|
||||
import { createLogsOverview } from './components/logs_overview';
|
||||
import { LogViewsService } from './services/log_views';
|
||||
import {
|
||||
LogsSharedClientCoreSetup,
|
||||
|
@ -51,8 +52,16 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass {
|
|||
}
|
||||
|
||||
public start(core: CoreStart, plugins: LogsSharedClientStartDeps) {
|
||||
const { http } = core;
|
||||
const { data, dataViews, discoverShared, observabilityAIAssistant, logsDataAccess } = plugins;
|
||||
const { http, settings } = core;
|
||||
const {
|
||||
charts,
|
||||
data,
|
||||
dataViews,
|
||||
discoverShared,
|
||||
logsDataAccess,
|
||||
observabilityAIAssistant,
|
||||
share,
|
||||
} = plugins;
|
||||
|
||||
const logViews = this.logViews.start({
|
||||
http,
|
||||
|
@ -61,9 +70,18 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass {
|
|||
search: data.search,
|
||||
});
|
||||
|
||||
const LogsOverview = createLogsOverview({
|
||||
charts,
|
||||
logsDataAccess,
|
||||
search: data.search.search,
|
||||
uiSettings: settings,
|
||||
share,
|
||||
});
|
||||
|
||||
if (!observabilityAIAssistant) {
|
||||
return {
|
||||
logViews,
|
||||
LogsOverview,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -77,6 +95,7 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass {
|
|||
return {
|
||||
logViews,
|
||||
LogAIAssistant,
|
||||
LogsOverview,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -5,19 +5,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
|
||||
import type { CoreSetup, CoreStart, Plugin as PluginClass } from '@kbn/core/public';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import type { DiscoverSharedPublicStart } from '@kbn/discover-shared-plugin/public';
|
||||
import { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public';
|
||||
import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public';
|
||||
import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public';
|
||||
import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
|
||||
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
|
||||
import { LogsSharedLocators } from '../common/locators';
|
||||
import type { LogsSharedLocators } from '../common/locators';
|
||||
import type { LogAIAssistantProps } from './components/log_ai_assistant/log_ai_assistant';
|
||||
// import type { OsqueryPluginStart } from '../../osquery/public';
|
||||
import { LogViewsServiceSetup, LogViewsServiceStart } from './services/log_views';
|
||||
import type { SelfContainedLogsOverview } from './components/logs_overview';
|
||||
import type { LogViewsServiceSetup, LogViewsServiceStart } from './services/log_views';
|
||||
|
||||
// Our own setup and start contract values
|
||||
export interface LogsSharedClientSetupExports {
|
||||
|
@ -28,6 +28,7 @@ export interface LogsSharedClientSetupExports {
|
|||
export interface LogsSharedClientStartExports {
|
||||
logViews: LogViewsServiceStart;
|
||||
LogAIAssistant?: (props: Omit<LogAIAssistantProps, 'observabilityAIAssistant'>) => JSX.Element;
|
||||
LogsOverview: SelfContainedLogsOverview;
|
||||
}
|
||||
|
||||
export interface LogsSharedClientSetupDeps {
|
||||
|
@ -35,6 +36,7 @@ export interface LogsSharedClientSetupDeps {
|
|||
}
|
||||
|
||||
export interface LogsSharedClientStartDeps {
|
||||
charts: ChartsPluginStart;
|
||||
data: DataPublicPluginStart;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
discoverShared: DiscoverSharedPublicStart;
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import { UiSettingsParams } from '@kbn/core-ui-settings-common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID } from '@kbn/management-settings-ids';
|
||||
|
||||
const technicalPreviewLabel = i18n.translate('xpack.logsShared.technicalPreviewSettingLabel', {
|
||||
defaultMessage: 'Technical Preview',
|
||||
});
|
||||
|
||||
export const featureFlagUiSettings: Record<string, UiSettingsParams> = {
|
||||
[OBSERVABILITY_LOGS_SHARED_NEW_LOGS_OVERVIEW_ID]: {
|
||||
category: ['observability'],
|
||||
name: i18n.translate('xpack.logsShared.newLogsOverviewSettingName', {
|
||||
defaultMessage: 'New logs overview',
|
||||
}),
|
||||
value: false,
|
||||
description: i18n.translate('xpack.logsShared.newLogsOverviewSettingDescription', {
|
||||
defaultMessage: '{technicalPreviewLabel} Enable the new logs overview experience.',
|
||||
|
||||
values: { technicalPreviewLabel: `<em>[${technicalPreviewLabel}]</em>` },
|
||||
}),
|
||||
type: 'boolean',
|
||||
schema: schema.boolean(),
|
||||
requiresPageReload: true,
|
||||
},
|
||||
};
|
|
@ -5,8 +5,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext, CoreStart, Plugin, Logger } from '@kbn/core/server';
|
||||
|
||||
import { CoreStart, Logger, Plugin, PluginInitializerContext } from '@kbn/core/server';
|
||||
import { defaultLogViewId } from '../common/log_views';
|
||||
import { LogsSharedConfig } from '../common/plugin_config';
|
||||
import { registerDeprecations } from './deprecations';
|
||||
import { featureFlagUiSettings } from './feature_flags';
|
||||
import { KibanaFramework } from './lib/adapters/framework/kibana_framework_adapter';
|
||||
import { LogsSharedKibanaLogEntriesAdapter } from './lib/adapters/log_entries/kibana_log_entries_adapter';
|
||||
import { LogsSharedLogEntriesDomain } from './lib/domains/log_entries_domain';
|
||||
import { LogsSharedBackendLibs, LogsSharedDomainLibs } from './lib/logs_shared_types';
|
||||
import { initLogsSharedServer } from './logs_shared_server';
|
||||
import { logViewSavedObjectType } from './saved_objects';
|
||||
import { LogEntriesService } from './services/log_entries';
|
||||
import { LogViewsService } from './services/log_views';
|
||||
import {
|
||||
LogsSharedPluginCoreSetup,
|
||||
LogsSharedPluginSetup,
|
||||
|
@ -15,17 +26,6 @@ import {
|
|||
LogsSharedServerPluginStartDeps,
|
||||
UsageCollector,
|
||||
} from './types';
|
||||
import { logViewSavedObjectType } from './saved_objects';
|
||||
import { initLogsSharedServer } from './logs_shared_server';
|
||||
import { LogViewsService } from './services/log_views';
|
||||
import { KibanaFramework } from './lib/adapters/framework/kibana_framework_adapter';
|
||||
import { LogsSharedBackendLibs, LogsSharedDomainLibs } from './lib/logs_shared_types';
|
||||
import { LogsSharedLogEntriesDomain } from './lib/domains/log_entries_domain';
|
||||
import { LogsSharedKibanaLogEntriesAdapter } from './lib/adapters/log_entries/kibana_log_entries_adapter';
|
||||
import { LogEntriesService } from './services/log_entries';
|
||||
import { LogsSharedConfig } from '../common/plugin_config';
|
||||
import { registerDeprecations } from './deprecations';
|
||||
import { defaultLogViewId } from '../common/log_views';
|
||||
|
||||
export class LogsSharedPlugin
|
||||
implements
|
||||
|
@ -88,6 +88,8 @@ export class LogsSharedPlugin
|
|||
|
||||
registerDeprecations({ core });
|
||||
|
||||
core.uiSettings.register(featureFlagUiSettings);
|
||||
|
||||
return {
|
||||
...domainLibs,
|
||||
logViews,
|
||||
|
|
|
@ -44,5 +44,9 @@
|
|||
"@kbn/logs-data-access-plugin",
|
||||
"@kbn/core-deprecations-common",
|
||||
"@kbn/core-deprecations-server",
|
||||
"@kbn/management-settings-ids",
|
||||
"@kbn/observability-logs-overview",
|
||||
"@kbn/charts-plugin",
|
||||
"@kbn/core-ui-settings-common",
|
||||
]
|
||||
}
|
||||
|
|
17
yarn.lock
17
yarn.lock
|
@ -5879,6 +5879,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/observability-logs-overview@link:x-pack/packages/observability/logs_overview":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/observability-onboarding-e2e@link:x-pack/plugins/observability_solution/observability_onboarding/e2e":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
@ -12105,6 +12109,14 @@
|
|||
use-isomorphic-layout-effect "^1.1.2"
|
||||
use-sync-external-store "^1.0.0"
|
||||
|
||||
"@xstate5/react@npm:@xstate/react@^4.1.2":
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@xstate/react/-/react-4.1.2.tgz#4bfcdf2d9e9ef1eaea7388d1896649345e6679cd"
|
||||
integrity sha512-orAidFrKCrU0ZwN5l/ABPlBfW2ziRDT2RrYoktRlZ0WRoLvA2E/uAC1JpZt43mCLtc8jrdwYCgJiqx1V8NvGTw==
|
||||
dependencies:
|
||||
use-isomorphic-layout-effect "^1.1.2"
|
||||
use-sync-external-store "^1.2.0"
|
||||
|
||||
"@xtuc/ieee754@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
|
||||
|
@ -32800,6 +32812,11 @@ xpath@^0.0.33:
|
|||
resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.33.tgz#5136b6094227c5df92002e7c3a13516a5074eb07"
|
||||
integrity sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA==
|
||||
|
||||
"xstate5@npm:xstate@^5.18.1", xstate@^5.18.1:
|
||||
version "5.18.1"
|
||||
resolved "https://registry.yarnpkg.com/xstate/-/xstate-5.18.1.tgz#c4d43ceaba6e6c31705d36bd96e285de4be4f7f4"
|
||||
integrity sha512-m02IqcCQbaE/kBQLunwub/5i8epvkD2mFutnL17Oeg1eXTShe1sRF4D5mhv1dlaFO4vbW5gRGRhraeAD5c938g==
|
||||
|
||||
xstate@^4.38.2:
|
||||
version "4.38.2"
|
||||
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.38.2.tgz#1b74544fc9c8c6c713ba77f81c6017e65aa89804"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue