Skip to content

Commit

Permalink
[Obs utils] Add Observability utils package (#189712)
Browse files Browse the repository at this point in the history
Adds a `@kbn/observability-utils` package.

```md
# @kbn/observability-utils

This package contains utilities for Observability plugins. It's a separate package
to get out of dependency hell. You can put anything in here that is stateless and
has no dependency on other plugins (either directly or via other packages).

The utility functions should be used via direct imports to minimize impact on
bundle size and limit the risk on importing browser code to the server and vice versa.
```

Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
dgieselaar and elasticmachine authored Aug 28, 2024
1 parent eeed6c8 commit ecec57c
Show file tree
Hide file tree
Showing 21 changed files with 524 additions and 3 deletions.
6 changes: 5 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -928,7 +928,10 @@ module.exports = {
},
},
{
files: ['x-pack/plugins/observability_solution/**/*.{ts,tsx}'],
files: [
'x-pack/plugins/observability_solution/**/*.{ts,tsx}',
'x-pack/packages/observability/**/*.{ts,tsx}',
],
rules: {
'react-hooks/exhaustive-deps': [
'error',
Expand All @@ -944,6 +947,7 @@ module.exports = {
'x-pack/plugins/aiops/**/*.tsx',
'x-pack/plugins/observability_solution/**/*.tsx',
'src/plugins/ai_assistant_management/**/*.tsx',
'x-pack/packages/observability/**/*.{ts,tsx}',
],
rules: {
'@kbn/telemetry/event_generating_elements_should_be_instrumented': 'error',
Expand Down
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,7 @@ x-pack/plugins/observability_solution/observability_onboarding/e2e @elastic/obs-
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
x-pack/plugins/observability_solution/observability_shared @elastic/observability-ui
x-pack/packages/observability/observability_utils @elastic/observability-ui
x-pack/test/security_api_integration/plugins/oidc_provider @elastic/kibana-security
test/common/plugins/otel_metrics @elastic/obs-ux-infra_services-team
packages/kbn-openapi-bundler @elastic/security-detection-rule-management
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@elastic/apm-rum": "^5.16.1",
"@elastic/apm-rum-core": "^5.21.0",
"@elastic/apm-rum-core": "^5.21.1",
"@elastic/apm-rum-react": "^2.0.3",
"@elastic/charts": "66.1.1",
"@elastic/datemath": "5.0.3",
Expand Down Expand Up @@ -674,6 +674,7 @@
"@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",
"@kbn/observability-utils": "link:x-pack/packages/observability/observability_utils",
"@kbn/oidc-provider-plugin": "link:x-pack/test/security_api_integration/plugins/oidc_provider",
"@kbn/open-telemetry-instrumented-plugin": "link:test/common/plugins/otel_metrics",
"@kbn/openapi-common": "link:packages/kbn-openapi-common",
Expand Down
2 changes: 2 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -1278,6 +1278,8 @@
"@kbn/observability-plugin/*": ["x-pack/plugins/observability_solution/observability/*"],
"@kbn/observability-shared-plugin": ["x-pack/plugins/observability_solution/observability_shared"],
"@kbn/observability-shared-plugin/*": ["x-pack/plugins/observability_solution/observability_shared/*"],
"@kbn/observability-utils": ["x-pack/packages/observability/observability_utils"],
"@kbn/observability-utils/*": ["x-pack/packages/observability/observability_utils/*"],
"@kbn/oidc-provider-plugin": ["x-pack/test/security_api_integration/plugins/oidc_provider"],
"@kbn/oidc-provider-plugin/*": ["x-pack/test/security_api_integration/plugins/oidc_provider/*"],
"@kbn/open-telemetry-instrumented-plugin": ["test/common/plugins/otel_metrics"],
Expand Down
5 changes: 5 additions & 0 deletions x-pack/packages/observability/observability_utils/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# @kbn/observability-utils

This package contains utilities for Observability plugins. It's a separate package to get out of dependency hell. You can put anything in here that is stateless and has no dependency on other plugins (either directly or via other packages).

The utility functions should be used via direct imports to minimize impact on bundle size and limit the risk on importing browser code to the server and vice versa.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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 { ElasticsearchClient, Logger } from '@kbn/core/server';
import type { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types';
import { withSpan } from '@kbn/apm-utils';

type SearchRequest = ESSearchRequest & {
index: string | string[];
track_total_hits: number | boolean;
size: number | boolean;
};

/**
* An Elasticsearch Client with a fully typed `search` method and built-in
* APM instrumentation.
*/
export interface ObservabilityElasticsearchClient {
search<TDocument = unknown, TSearchRequest extends SearchRequest = SearchRequest>(
operationName: string,
parameters: TSearchRequest
): Promise<InferSearchResponseOf<TDocument, TSearchRequest>>;
client: ElasticsearchClient;
}

export function createObservabilityEsClient({
client,
logger,
plugin,
}: {
client: ElasticsearchClient;
logger: Logger;
plugin: string;
}): ObservabilityElasticsearchClient {
return {
client,
search<TDocument = unknown, TSearchRequest extends SearchRequest = SearchRequest>(
operationName: string,
parameters: SearchRequest
) {
logger.trace(() => `Request (${operationName}):\n${JSON.stringify(parameters, null, 2)}`);
// wraps the search operation in a named APM span for better analysis
// (otherwise it would just be a _search span)
return withSpan(
{
name: operationName,
labels: {
plugin,
},
},
() => {
return client.search<TDocument>(parameters) as unknown as Promise<
InferSearchResponseOf<TDocument, TSearchRequest>
>;
}
).then((response) => {
logger.trace(() => `Response (${operationName}):\n${JSON.stringify(response, null, 2)}`);
return response;
});
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* 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 { estypes } from '@elastic/elasticsearch';
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';

export function kqlQuery(kql?: string): estypes.QueryDslQueryContainer[] {
if (!kql) {
return [];
}

const ast = fromKueryExpression(kql);
return [toElasticsearchQuery(ast)];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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 { estypes } from '@elastic/elasticsearch';

export function rangeQuery(
start?: number,
end?: number,
field = '@timestamp'
): estypes.QueryDslQueryContainer[] {
return [
{
range: {
[field]: {
gte: start,
lte: end,
format: 'epoch_millis',
},
},
},
];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';

interface TermQueryOpts {
queryEmptyString: boolean;
}

export function termQuery<T extends string>(
field: T,
value: string | boolean | number | undefined | null,
opts: TermQueryOpts = { queryEmptyString: true }
): QueryDslQueryContainer[] {
if (value === null || value === undefined || (!opts.queryEmptyString && value === '')) {
return [];
}

return [{ term: { [field]: value } }];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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 { useEffect, useState } from 'react';

export function useAbortController() {
const [controller, setController] = useState(() => new AbortController());

useEffect(() => {
return () => {
controller.abort();
};
}, [controller]);

return {
signal: controller.signal,
refresh: () => {
setController(() => new AbortController());
},
};
}
Original file line number Diff line number Diff line change
@@ -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 { isPromise } from '@kbn/std';
import { useEffect, useMemo, useRef, useState } from 'react';

interface State<T> {
error?: Error;
value?: T;
loading: boolean;
}

export type AbortableAsyncState<T> = (T extends Promise<infer TReturn>
? State<TReturn>
: State<T>) & { refresh: () => void };

export function useAbortableAsync<T>(
fn: ({}: { signal: AbortSignal }) => T | Promise<T>,
deps: any[],
options?: { clearValueOnNext?: boolean; defaultValue?: () => T }
): AbortableAsyncState<T> {
const clearValueOnNext = options?.clearValueOnNext;

const controllerRef = useRef(new AbortController());

const [refreshId, setRefreshId] = useState(0);

const [error, setError] = useState<Error>();
const [loading, setLoading] = useState(false);
const [value, setValue] = useState<T | undefined>(options?.defaultValue);

useEffect(() => {
controllerRef.current.abort();

const controller = new AbortController();
controllerRef.current = controller;

if (clearValueOnNext) {
setValue(undefined);
setError(undefined);
}

try {
const response = fn({ signal: controller.signal });
if (isPromise(response)) {
setLoading(true);
response
.then((nextValue) => {
setError(undefined);
setValue(nextValue);
})
.catch((err) => {
setValue(undefined);
setError(err);
})
.finally(() => setLoading(false));
} else {
setError(undefined);
setValue(response);
setLoading(false);
}
} catch (err) {
setValue(undefined);
setError(err);
setLoading(false);
}

return () => {
controller.abort();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps.concat(refreshId, clearValueOnNext));

return useMemo<AbortableAsyncState<T>>(() => {
return {
error,
loading,
value,
refresh: () => {
setRefreshId((id) => id + 1);
},
} as unknown as AbortableAsyncState<T>;
}, [error, value, loading]);
}
Original file line number Diff line number Diff line change
@@ -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.
*/

import { useEuiTheme } from '@elastic/eui';

export function useTheme() {
return useEuiTheme().euiTheme;
}
12 changes: 12 additions & 0 deletions x-pack/packages/observability/observability_utils/jest.config.js
Original file line number Diff line number Diff line change
@@ -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/observability_utils'],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/observability-utils",
"owner": "@elastic/observability-ui"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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 { flattenObject } from './flatten_object';

describe('flattenObject', () => {
it('flattens deeply nested objects', () => {
expect(
flattenObject({
first: {
second: {
third: 'third',
},
},
})
).toEqual({
'first.second.third': 'third',
});
});

it('does not flatten arrays', () => {
expect(
flattenObject({
simpleArray: ['0', '1', '2'],
complexArray: [{ one: 'one', two: 'two', three: 'three' }],
nested: {
array: [0, 1, 2],
},
})
).toEqual({
simpleArray: ['0', '1', '2'],
complexArray: [{ one: 'one', two: 'two', three: 'three' }],
'nested.array': [0, 1, 2],
});
});
});
Loading

0 comments on commit ecec57c

Please sign in to comment.