Skip to content

Commit

Permalink
[APM] Alert annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
dgieselaar committed May 19, 2020
1 parent 3350bff commit 6cc089b
Show file tree
Hide file tree
Showing 14 changed files with 117 additions and 61 deletions.
3 changes: 2 additions & 1 deletion x-pack/plugins/apm/common/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
*/

export enum AnnotationType {
VERSION = 'version'
VERSION = 'version',
ALERT = 'alert'
}

export interface Annotation {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Maybe } from '../../../../../typings/common';
import { Annotation } from '../../../../../common/annotations';
import { Annotation, AnnotationType } from '../../../../../common/annotations';
import { PlotValues, SharedPlot } from './plotUtils';
import { asAbsoluteDateTime } from '../../../../utils/formatters';

Expand All @@ -31,6 +31,11 @@ const style = {
strokeDasharray: 'none'
};

const colorMap = {
[AnnotationType.VERSION]: theme.euiColorSecondary,
[AnnotationType.ALERT]: theme.euiColorVis3
};

export function AnnotationsPlot(props: Props) {
const { plotValues, annotations } = props;

Expand All @@ -56,16 +61,16 @@ export function AnnotationsPlot(props: Props) {
<EuiFlexGroup>
<EuiFlexItem grow={true}>
<EuiText>
{i18n.translate('xpack.apm.version', {
defaultMessage: 'Version'
{i18n.translate('xpack.apm.annotation.message', {
defaultMessage: 'Message'
})}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>{annotation.text}</EuiFlexItem>
</EuiFlexGroup>
}
>
<EuiIcon type="dot" color={theme.euiColorSecondary} />
<EuiIcon type="dot" color={colorMap[annotation.type]} />
</EuiToolTip>
</div>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ export default function Legends({
}}
text={
<LegendContent>
{i18n.translate('xpack.apm.serviceVersion', {
defaultMessage: 'Service version'
{i18n.translate('xpack.apm.legends.annotations', {
defaultMessage: 'Annotations'
})}
</LegendContent>
}
Expand Down
6 changes: 4 additions & 2 deletions x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,27 @@
*/

import { Observable } from 'rxjs';
import { ObservabilityPluginSetup } from '../../../../observability/server';
import { AlertingPlugin } from '../../../../alerting/server';
import { ActionsPlugin } from '../../../../actions/server';
import { registerTransactionDurationAlertType } from './register_transaction_duration_alert_type';
import { registerErrorRateAlertType } from './register_error_rate_alert_type';
import { APMConfig } from '../..';

interface Params {
alerting: AlertingPlugin['setup'];
actions: ActionsPlugin['setup'];
observability?: ObservabilityPluginSetup;
config$: Observable<APMConfig>;
}

export function registerApmAlerts(params: Params) {
registerTransactionDurationAlertType({
alerting: params.alerting,
observability: params.observability,
config$: params.config$
});
registerErrorRateAlertType({
alerting: params.alerting,
observability: params.observability,
config$: params.config$
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { schema, TypeOf } from '@kbn/config-schema';
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { i18n } from '@kbn/i18n';
import { ObservabilityPluginSetup } from '../../../../observability/server';
import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values';
import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types';
import {
Expand All @@ -25,6 +26,7 @@ import { APMConfig } from '../..';

interface RegisterAlertParams {
alerting: AlertingPlugin['setup'];
observability?: ObservabilityPluginSetup;
config$: Observable<APMConfig>;
}

Expand All @@ -40,6 +42,7 @@ const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.ErrorRate];

export function registerErrorRateAlertType({
alerting,
observability,
config$
}: RegisterAlertParams) {
alerting.registerType({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { schema, TypeOf } from '@kbn/config-schema';
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { i18n } from '@kbn/i18n';
import { ObservabilityPluginSetup } from '../../../../observability/server';
import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values';
import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types';
import { ESSearchResponse } from '../../../typings/elasticsearch';
Expand All @@ -24,6 +25,7 @@ import { APMConfig } from '../..';

interface RegisterAlertParams {
alerting: AlertingPlugin['setup'];
observability?: ObservabilityPluginSetup;
config$: Observable<APMConfig>;
}

Expand All @@ -44,6 +46,7 @@ const paramsSchema = schema.object({
const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.TransactionDuration];

export function registerTransactionDurationAlertType({
observability,
alerting,
config$
}: RegisterAlertParams) {
Expand Down Expand Up @@ -81,17 +84,25 @@ export function registerTransactionDurationAlertType({
executor: async ({ services, params }) => {
const config = await config$.pipe(take(1)).toPromise();

const alertParams = params as TypeOf<typeof paramsSchema>;
const {
serviceName,
environment,
threshold,
transactionType,
windowSize,
windowUnit,
aggregationType
} = params as TypeOf<typeof paramsSchema>;

const indices = await getApmIndices({
config,
savedObjectsClient: services.savedObjectsClient
});

const environmentTerm =
alertParams.environment === ENVIRONMENT_ALL
environment === ENVIRONMENT_ALL
? []
: [{ term: { [SERVICE_ENVIRONMENT]: alertParams.environment } }];
: [{ term: { [SERVICE_ENVIRONMENT]: environment } }];

const searchParams = {
index: indices['apm_oss.transactionIndices'],
Expand All @@ -103,7 +114,7 @@ export function registerTransactionDurationAlertType({
{
range: {
'@timestamp': {
gte: `now-${alertParams.windowSize}${alertParams.windowUnit}`
gte: `now-${windowSize}${windowUnit}`
}
}
},
Expand All @@ -114,12 +125,12 @@ export function registerTransactionDurationAlertType({
},
{
term: {
[SERVICE_NAME]: alertParams.serviceName
[SERVICE_NAME]: serviceName
}
},
{
term: {
[TRANSACTION_TYPE]: alertParams.transactionType
[TRANSACTION_TYPE]: transactionType
}
},
...environmentTerm
Expand All @@ -128,7 +139,7 @@ export function registerTransactionDurationAlertType({
},
aggs: {
agg:
alertParams.aggregationType === 'avg'
aggregationType === 'avg'
? {
avg: {
field: TRANSACTION_DURATION
Expand All @@ -137,9 +148,7 @@ export function registerTransactionDurationAlertType({
: {
percentiles: {
field: TRANSACTION_DURATION,
percents: [
alertParams.aggregationType === '95th' ? 95 : 99
]
percents: [aggregationType === '95th' ? 95 : 99]
}
}
}
Expand All @@ -159,13 +168,39 @@ export function registerTransactionDurationAlertType({

const value = 'values' in agg ? agg.values[0] : agg.value;

if (value && value > alertParams.threshold * 1000) {
if (value && value > threshold * 1000) {
const alertInstance = services.alertInstanceFactory(
AlertType.TransactionDuration
);

alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, {
transactionType: alertParams.transactionType,
serviceName: alertParams.serviceName
transactionType,
serviceName
});

const annotationsClient = await observability?.getScopedAnnotationsClient(
services.callCluster
);

await annotationsClient?.create({
'@timestamp': new Date().toISOString(),
annotation: {
type: 'alert'
},
message: i18n.translate(
'xpack.apm.registerTransactionDurationAlertType.annotation',
{
defaultMessage: `Transaction duration exceeded`
}
),
tags: ['apm'],
service: {
name: serviceName,
...(environment !== ENVIRONMENT_ALL ? { environment } : {})
},
transaction: {
type: transactionType
}
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export async function getStoredAnnotations({
}
}
},
{ term: { 'annotation.type': 'deployment' } },
{ terms: { 'annotation.type': ['deployment', 'alert'] } },
{ term: { tags: 'apm' } },
{ term: { [SERVICE_NAME]: serviceName } },
...(environmentFilter ? [environmentFilter] : [])
Expand All @@ -59,7 +59,10 @@ export async function getStoredAnnotations({

return response.hits.hits.map(hit => {
return {
type: AnnotationType.VERSION,
type:
hit._source.annotation.type === 'deployment'
? AnnotationType.VERSION
: AnnotationType.ALERT,
id: hit._id,
'@timestamp': new Date(hit._source['@timestamp']).getTime(),
text: hit._source.message
Expand Down
6 changes: 2 additions & 4 deletions x-pack/plugins/apm/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { SecurityPluginSetup } from '../../security/public';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
import { TaskManagerSetupContract } from '../../task_manager/server';
import { AlertingPlugin } from '../../alerting/server';
import { ActionsPlugin } from '../../actions/server';
import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server';
import { createApmAgentConfigurationIndex } from './lib/settings/agent_configuration/create_agent_config_index';
import { createApmCustomLinkIndex } from './lib/settings/custom_link/create_custom_link_index';
Expand Down Expand Up @@ -57,7 +56,6 @@ export class APMPlugin implements Plugin<APMPluginSetup> {
usageCollection?: UsageCollectionSetup;
taskManager?: TaskManagerSetupContract;
alerting?: AlertingPlugin['setup'];
actions?: ActionsPlugin['setup'];
observability?: ObservabilityPluginSetup;
features: FeaturesPluginSetup;
security?: SecurityPluginSetup;
Expand All @@ -72,10 +70,10 @@ export class APMPlugin implements Plugin<APMPluginSetup> {
core.savedObjects.registerType(apmIndices);
core.savedObjects.registerType(apmTelemetry);

if (plugins.actions && plugins.alerting) {
if (plugins.alerting) {
registerApmAlerts({
alerting: plugins.alerting,
actions: plugins.actions,
observability: plugins.observability,
config$: mergedConfig$
});
}
Expand Down
6 changes: 2 additions & 4 deletions x-pack/plugins/apm/server/routes/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,7 @@ export const serviceAnnotationsRoute = createRoute(() => ({
const { environment } = context.params.query;

const annotationsClient = await context.plugins.observability?.getScopedAnnotationsClient(
context,
request
context.core.elasticsearch.dataClient.callAsCurrentUser
);

return getServiceAnnotations({
Expand Down Expand Up @@ -139,8 +138,7 @@ export const serviceAnnotationsCreateRoute = createRoute(() => ({
},
handler: async ({ request, context }) => {
const annotationsClient = await context.plugins.observability?.getScopedAnnotationsClient(
context,
request
context.core.elasticsearch.dataClient.callAsCurrentUser
);

if (!annotationsClient) {
Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/observability/common/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export const createAnnotationRt = t.intersection([
environment: t.string,
version: t.string,
}),
transaction: t.type({
type: t.string,
}),
}),
]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
CoreSetup,
PluginInitializerContext,
KibanaRequest,
RequestHandlerContext,
} from 'kibana/server';
import { CoreSetup, PluginInitializerContext, APICaller } from 'kibana/server';
import { ILicense } from '../../../../licensing/public';
import { PromiseReturnType } from '../../../typings/common';
import { createAnnotationsClient } from './create_annotations_client';
import { registerAnnotationAPIs } from './register_annotation_apis';
Expand All @@ -19,7 +15,7 @@ interface Params {
context: PluginInitializerContext;
}

export type ScopedAnnotationsClientFactory = PromiseReturnType<
type ScopedAnnotationsClientFactory = PromiseReturnType<
typeof bootstrapAnnotations
>['getScopedAnnotationsClient'];

Expand All @@ -36,12 +32,18 @@ export async function bootstrapAnnotations({ index, core, context }: Params) {
});

return {
getScopedAnnotationsClient: (requestContext: RequestHandlerContext, request: KibanaRequest) => {
getScopedAnnotationsClient: ({
apiCaller,
license,
}: {
apiCaller: APICaller;
license?: ILicense;
}) => {
return createAnnotationsClient({
index,
apiCaller: core.elasticsearch.dataClient.asScoped(request).callAsCurrentUser,
apiCaller,
logger,
license: requestContext.licensing?.license,
license,
});
},
};
Expand Down
Loading

0 comments on commit 6cc089b

Please sign in to comment.