Skip to content

Commit

Permalink
[APM] Annotations API (#64796)
Browse files Browse the repository at this point in the history
  • Loading branch information
dgieselaar authored May 5, 2020
1 parent af8f9fa commit 399eed7
Show file tree
Hide file tree
Showing 51 changed files with 1,640 additions and 266 deletions.
2 changes: 1 addition & 1 deletion x-pack/plugins/apm/common/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ export enum AnnotationType {
export interface Annotation {
type: AnnotationType;
id: string;
time: number;
'@timestamp': number;
text: string;
}
6 changes: 5 additions & 1 deletion x-pack/plugins/apm/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@
"taskManager",
"actions",
"alerting",
"observability",
"security"
],
"server": true,
"ui": true,
"configPath": ["xpack", "apm"]
"configPath": [
"xpack",
"apm"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const style = {
export function AnnotationsPlot(props: Props) {
const { plotValues, annotations } = props;

const tickValues = annotations.map(annotation => annotation.time);
const tickValues = annotations.map(annotation => annotation['@timestamp']);

return (
<>
Expand All @@ -46,12 +46,12 @@ export function AnnotationsPlot(props: Props) {
key={annotation.id}
style={{
position: 'absolute',
left: plotValues.x(annotation.time) - 8,
left: plotValues.x(annotation['@timestamp']) - 8,
top: -2
}}
>
<EuiToolTip
title={asAbsoluteDateTime(annotation.time, 'seconds')}
title={asAbsoluteDateTime(annotation['@timestamp'], 'seconds')}
content={
<EuiFlexGroup>
<EuiFlexItem grow={true}>
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/apm/public/context/ChartsSyncContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const ChartsSyncContextProvider: React.FC = ({ children }) => {
callApmApi => {
if (start && end && serviceName) {
return callApmApi({
pathname: '/api/apm/services/{serviceName}/annotations',
pathname: '/api/apm/services/{serviceName}/annotation/search',
params: {
path: {
serviceName
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/apm/scripts/optimize-tsconfig/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"include": [
"./plugins/apm/**/*",
"./plugins/observability/**/*",
"./typings/**/*"
],
"exclude": [
Expand Down
105 changes: 0 additions & 105 deletions x-pack/plugins/apm/server/lib/helpers/create_or_update_index.ts

This file was deleted.

8 changes: 5 additions & 3 deletions x-pack/plugins/apm/server/lib/helpers/es_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
/* eslint-disable no-console */
import {
IndexDocumentParams,
IndicesDeleteParams,
SearchParams,
IndicesCreateParams,
DeleteDocumentResponse
DeleteDocumentResponse,
DeleteDocumentParams
} from 'elasticsearch';
import { cloneDeep, isString, merge } from 'lodash';
import { KibanaRequest } from 'src/core/server';
Expand Down Expand Up @@ -204,7 +204,9 @@ export function getESClient(
index: <Body>(params: APMIndexDocumentParams<Body>) => {
return callEs('index', params);
},
delete: (params: IndicesDeleteParams): Promise<DeleteDocumentResponse> => {
delete: (
params: Omit<DeleteDocumentParams, 'type'>
): Promise<DeleteDocumentResponse> => {
return callEs('delete', params);
},
indicesCreate: (params: IndicesCreateParams) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { isNumber } from 'lodash';
import { Annotation, AnnotationType } from '../../../../common/annotations';
import { SetupTimeRange, Setup } from '../../helpers/setup_request';
import { ESFilter } from '../../../../typings/elasticsearch';
import { rangeFilter } from '../../helpers/range_filter';
import {
PROCESSOR_EVENT,
SERVICE_NAME,
SERVICE_VERSION
} from '../../../../common/elasticsearch_fieldnames';
import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es';

export async function getDerivedServiceAnnotations({
setup,
serviceName,
environment
}: {
serviceName: string;
environment?: string;
setup: Setup & SetupTimeRange;
}) {
const { start, end, client, indices } = setup;

const filter: ESFilter[] = [
{ term: { [PROCESSOR_EVENT]: 'transaction' } },
{ term: { [SERVICE_NAME]: serviceName } }
];

const environmentFilter = getEnvironmentUiFilterES(environment);

if (environmentFilter) {
filter.push(environmentFilter);
}

const versions =
(
await client.search({
index: indices['apm_oss.transactionIndices'],
body: {
size: 0,
query: {
bool: {
filter: filter.concat({ range: rangeFilter(start, end) })
}
},
aggs: {
versions: {
terms: {
field: SERVICE_VERSION
}
}
}
}
})
).aggregations?.versions.buckets.map(bucket => bucket.key) ?? [];

if (versions.length <= 1) {
return [];
}
const annotations = await Promise.all(
versions.map(async version => {
const response = await client.search({
index: indices['apm_oss.transactionIndices'],
body: {
size: 0,
query: {
bool: {
filter: filter.concat({
term: {
[SERVICE_VERSION]: version
}
})
}
},
aggs: {
first_seen: {
min: {
field: '@timestamp'
}
}
}
}
});

const firstSeen = response.aggregations?.first_seen.value;

if (!isNumber(firstSeen)) {
throw new Error(
'First seen for version was unexpectedly undefined or null.'
);
}

if (firstSeen < start || firstSeen > end) {
return null;
}

return {
type: AnnotationType.VERSION,
id: version,
'@timestamp': firstSeen,
text: version
};
})
);
return annotations.filter(Boolean) as Annotation[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { APICaller } from 'kibana/server';
import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames';
import { ESSearchResponse } from '../../../../typings/elasticsearch';
import { ScopedAnnotationsClient } from '../../../../../observability/server';
import { Annotation, AnnotationType } from '../../../../common/annotations';
import { Annotation as ESAnnotation } from '../../../../../observability/common/annotations';
import { SetupTimeRange, Setup } from '../../helpers/setup_request';
import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es';

export async function getStoredAnnotations({
setup,
serviceName,
environment,
apiCaller,
annotationsClient
}: {
setup: Setup & SetupTimeRange;
serviceName: string;
environment?: string;
apiCaller: APICaller;
annotationsClient: ScopedAnnotationsClient;
}): Promise<Annotation[]> {
try {
const environmentFilter = getEnvironmentUiFilterES(environment);

const response: ESSearchResponse<ESAnnotation, any> = (await apiCaller(
'search',
{
index: annotationsClient.index,
body: {
size: 50,
query: {
bool: {
filter: [
{
range: {
'@timestamp': {
gte: setup.start,
lt: setup.end
}
}
},
{ term: { 'annotation.type': 'deployment' } },
{ term: { tags: 'apm' } },
{ term: { [SERVICE_NAME]: serviceName } },
...(environmentFilter ? [environmentFilter] : [])
]
}
}
}
}
)) as any;

return response.hits.hits.map(hit => {
return {
type: AnnotationType.VERSION,
id: hit._id,
'@timestamp': new Date(hit._source['@timestamp']).getTime(),
text: hit._source.message
};
});
} catch (error) {
// index is only created when an annotation has been indexed,
// so we should handle this error gracefully
if (error.body?.error?.type === 'index_not_found_exception') {
return [];
}
throw error;
}
}
Loading

0 comments on commit 399eed7

Please sign in to comment.