mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Added Azure Functions support in the APM UI (#149479)
## Summary Adds support for Azure Functions in the APM UI. - adds serverless Azure Functions icon to the service_icons <img width="584" alt="image" src="https://user-images.githubusercontent.com/866830/214506733-ab50c9b3-977b-4730-9119-fef505329c58.png"> - replaces 'time spent by span type' graph with 'cold start rate' graph in service overview and transaction overview: <img width="891" alt="image" src="https://user-images.githubusercontent.com/866830/214507066-1a283f10-7a24-4b56-a5e4-85f94bb66724.png"> <img width="1549" alt="image" src="https://user-images.githubusercontent.com/866830/214507105-816341b4-09aa-48a6-b9c2-494d3115ebd6.png"> - adds synthtrace scenario for Azure Functions ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
5c97c487e8
commit
7ee827b844
28 changed files with 452 additions and 82 deletions
|
@ -27,14 +27,19 @@ export function serverlessFunction({
|
|||
environment,
|
||||
agentName,
|
||||
architecture = 'arm',
|
||||
serverlessType = 'aws.lambda',
|
||||
}: {
|
||||
functionName: string;
|
||||
environment: string;
|
||||
agentName: string;
|
||||
serviceName?: string;
|
||||
architecture?: string;
|
||||
serverlessType?: 'aws.lambda' | 'azure.functions';
|
||||
}) {
|
||||
const faasId = `arn:aws:lambda:us-west-2:001:function:${functionName}`;
|
||||
const faasId =
|
||||
serverlessType === 'aws.lambda'
|
||||
? `arn:aws:lambda:us-west-2:001:function:${functionName}`
|
||||
: `/subscriptions/abcd/resourceGroups/1234/providers/Microsoft.Web/sites/test-function-app/functions/${functionName}`;
|
||||
return new ServerlessFunction({
|
||||
'service.name': serviceName || faasId,
|
||||
'faas.id': faasId,
|
||||
|
|
|
@ -30,6 +30,7 @@ const scenario: Scenario<ApmFields> = async (runOptions: RunOptions) => {
|
|||
environment: ENVIRONMENT,
|
||||
agentName: 'python',
|
||||
functionName: 'fn-python-1',
|
||||
serverlessType: 'aws.lambda',
|
||||
})
|
||||
.instance({ instanceName: 'instance_A', ...cloudFields });
|
||||
|
||||
|
@ -39,6 +40,7 @@ const scenario: Scenario<ApmFields> = async (runOptions: RunOptions) => {
|
|||
environment: ENVIRONMENT,
|
||||
agentName: 'nodejs',
|
||||
functionName: 'fn-node-1',
|
||||
serverlessType: 'aws.lambda',
|
||||
})
|
||||
.instance({ instanceName: 'instance_A', ...cloudFields });
|
||||
|
||||
|
@ -47,6 +49,7 @@ const scenario: Scenario<ApmFields> = async (runOptions: RunOptions) => {
|
|||
environment: ENVIRONMENT,
|
||||
agentName: 'nodejs',
|
||||
functionName: 'fn-node-2',
|
||||
serverlessType: 'aws.lambda',
|
||||
})
|
||||
.instance({ instanceName: 'instance_A', ...cloudFields });
|
||||
|
||||
|
|
69
packages/kbn-apm-synthtrace/src/scenarios/azure_functions.ts
Normal file
69
packages/kbn-apm-synthtrace/src/scenarios/azure_functions.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { apm, ApmFields } from '@kbn/apm-synthtrace-client';
|
||||
import { Scenario } from '../cli/scenario';
|
||||
import { RunOptions } from '../cli/utils/parse_run_cli_flags';
|
||||
import { getSynthtraceEnvironment } from '../lib/utils/get_synthtrace_environment';
|
||||
|
||||
const ENVIRONMENT = getSynthtraceEnvironment(__filename);
|
||||
|
||||
const scenario: Scenario<ApmFields> = async (runOptions: RunOptions) => {
|
||||
return {
|
||||
generate: ({ range }) => {
|
||||
const timestamps = range.ratePerMinute(180);
|
||||
|
||||
const cloudFields: ApmFields = {
|
||||
'cloud.provider': 'azure',
|
||||
'cloud.service.name': 'functions',
|
||||
'cloud.region': 'Central US',
|
||||
};
|
||||
|
||||
const instanceALambdaDotnet = apm
|
||||
.serverlessFunction({
|
||||
serviceName: 'azure-functions',
|
||||
environment: ENVIRONMENT,
|
||||
agentName: 'dotnet',
|
||||
functionName: 'fn-dotnet-1',
|
||||
serverlessType: 'azure.functions',
|
||||
})
|
||||
.instance({ instanceName: 'instance_A', ...cloudFields });
|
||||
|
||||
const instanceALambdaDotnet2 = apm
|
||||
.serverlessFunction({
|
||||
serviceName: 'azure-functions',
|
||||
environment: ENVIRONMENT,
|
||||
agentName: 'dotnet',
|
||||
functionName: 'fn-dotnet-2',
|
||||
serverlessType: 'azure.functions',
|
||||
})
|
||||
.instance({ instanceName: 'instance_A', ...cloudFields });
|
||||
|
||||
const instanceALambdaNode2 = apm
|
||||
.serverlessFunction({
|
||||
environment: ENVIRONMENT,
|
||||
agentName: 'nodejs',
|
||||
functionName: 'fn-node-1',
|
||||
serverlessType: 'azure.functions',
|
||||
})
|
||||
.instance({ instanceName: 'instance_A', ...cloudFields });
|
||||
|
||||
const awsLambdaEvents = timestamps.generator((timestamp) => {
|
||||
return [
|
||||
instanceALambdaDotnet.invocation().duration(1000).timestamp(timestamp).coldStart(true),
|
||||
instanceALambdaDotnet2.invocation().duration(1000).timestamp(timestamp).coldStart(false),
|
||||
instanceALambdaNode2.invocation().duration(1000).timestamp(timestamp).coldStart(false),
|
||||
];
|
||||
});
|
||||
|
||||
return awsLambdaEvents;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default scenario;
|
|
@ -12,8 +12,12 @@ import {
|
|||
isAndroidAgentName,
|
||||
isMobileAgentName,
|
||||
isServerlessAgent,
|
||||
isAWSLambdaAgent,
|
||||
isAzureFunctionsAgent,
|
||||
} from './agent_name';
|
||||
|
||||
import { ServerlessType } from './serverless';
|
||||
|
||||
describe('agent name helpers', () => {
|
||||
describe('isJavaAgentName', () => {
|
||||
describe('when the agent name is java', () => {
|
||||
|
@ -146,27 +150,63 @@ describe('agent name helpers', () => {
|
|||
});
|
||||
|
||||
describe('isServerlessAgent', () => {
|
||||
describe('when the runtime name is AWS_LAMBDA', () => {
|
||||
describe('when the serverlessType is AWS_LAMBDA', () => {
|
||||
it('returns true', () => {
|
||||
expect(isServerlessAgent('AWS_LAMBDA')).toEqual(true);
|
||||
expect(isServerlessAgent(ServerlessType.AWS_LAMBDA)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the runtime name is aws_lambda', () => {
|
||||
describe('when the serverlessType is AZURE_FUNCTIONS', () => {
|
||||
it('returns true', () => {
|
||||
expect(isServerlessAgent('aws_lambda')).toEqual(true);
|
||||
expect(isServerlessAgent(ServerlessType.AZURE_FUNCTIONS)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the runtime name is aws_lambda_test', () => {
|
||||
it('returns true', () => {
|
||||
expect(isServerlessAgent('aws_lambda_test')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the runtime name is something else', () => {
|
||||
describe('when the serverlessType is undefined', () => {
|
||||
it('returns false', () => {
|
||||
expect(isServerlessAgent('not_aws_lambda')).toEqual(false);
|
||||
expect(isServerlessAgent(undefined)).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAWSLambdaAgent', () => {
|
||||
describe('when the serverlessType is AWS_LAMBDA', () => {
|
||||
it('returns true', () => {
|
||||
expect(isAWSLambdaAgent(ServerlessType.AWS_LAMBDA)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the serverlessType is AZURE_FUNCTIONS', () => {
|
||||
it('returns true', () => {
|
||||
expect(isAWSLambdaAgent(ServerlessType.AZURE_FUNCTIONS)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the serverlessType is undefined', () => {
|
||||
it('returns false', () => {
|
||||
expect(isAWSLambdaAgent(undefined)).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAzureFunctionsAgent', () => {
|
||||
describe('when the serverlessType is AZURE_FUNCTIONS', () => {
|
||||
it('returns true', () => {
|
||||
expect(isAzureFunctionsAgent(ServerlessType.AZURE_FUNCTIONS)).toEqual(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the serverlessType is AWS_LAMBDA', () => {
|
||||
it('returns true', () => {
|
||||
expect(isAzureFunctionsAgent(ServerlessType.AWS_LAMBDA)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the serverlessType is undefined', () => {
|
||||
it('returns false', () => {
|
||||
expect(isAzureFunctionsAgent(undefined)).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { AgentName } from '../typings/es_schemas/ui/fields/agent';
|
||||
import { ServerlessType } from './serverless';
|
||||
|
||||
/*
|
||||
* Agent names can be any string. This list only defines the official agents
|
||||
|
@ -81,8 +82,18 @@ export function isJRubyAgent(agentName?: string, runtimeName?: string) {
|
|||
return agentName === 'ruby' && runtimeName?.toLowerCase() === 'jruby';
|
||||
}
|
||||
|
||||
export function isServerlessAgent(runtimeName?: string) {
|
||||
return runtimeName?.toLowerCase().startsWith('aws_lambda');
|
||||
export function isServerlessAgent(serverlessType?: ServerlessType) {
|
||||
return (
|
||||
isAWSLambdaAgent(serverlessType) || isAzureFunctionsAgent(serverlessType)
|
||||
);
|
||||
}
|
||||
|
||||
export function isAWSLambdaAgent(serverlessType?: ServerlessType) {
|
||||
return serverlessType === ServerlessType.AWS_LAMBDA;
|
||||
}
|
||||
|
||||
export function isAzureFunctionsAgent(serverlessType?: ServerlessType) {
|
||||
return serverlessType === ServerlessType.AZURE_FUNCTIONS;
|
||||
}
|
||||
|
||||
export function isAndroidAgentName(agentName?: string) {
|
||||
|
|
|
@ -15,3 +15,27 @@ export function getServerlessFunctionNameFromId(serverlessId: string) {
|
|||
const match = serverlessIdRegex.exec(serverlessId);
|
||||
return match ? match[1] : serverlessId;
|
||||
}
|
||||
|
||||
export enum ServerlessType {
|
||||
AWS_LAMBDA = 'aws.lambda',
|
||||
AZURE_FUNCTIONS = 'azure.functions',
|
||||
}
|
||||
|
||||
export function getServerlessTypeFromCloudData(
|
||||
cloudProvider?: string,
|
||||
cloudServiceName?: string
|
||||
): ServerlessType | undefined {
|
||||
if (
|
||||
cloudProvider?.toLowerCase() === 'aws' &&
|
||||
cloudServiceName?.toLowerCase() === 'lambda'
|
||||
) {
|
||||
return ServerlessType.AWS_LAMBDA;
|
||||
}
|
||||
|
||||
if (
|
||||
cloudProvider?.toLowerCase() === 'azure' &&
|
||||
cloudServiceName?.toLowerCase() === 'functions'
|
||||
) {
|
||||
return ServerlessType.AZURE_FUNCTIONS;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,8 @@ export function generateData({ start, end }: { start: number; end: number }) {
|
|||
.transaction({ transactionName: transaction.name })
|
||||
.defaults({
|
||||
'service.runtime.name': 'AWS_Lambda_python3.8',
|
||||
'cloud.provider': 'aws',
|
||||
'cloud.service.name': 'lambda',
|
||||
'faas.coldstart': true,
|
||||
})
|
||||
.timestamp(timestamp)
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 url from 'url';
|
||||
import { synthtrace } from '../../../../../synthtrace';
|
||||
import { generateData } from './generate_data';
|
||||
|
||||
const start = '2021-10-10T00:00:00.000Z';
|
||||
const end = '2021-10-10T00:15:00.000Z';
|
||||
|
||||
const serviceOverviewHref = url.format({
|
||||
pathname: '/app/apm/services/synth-dotnet/overview',
|
||||
query: { rangeFrom: start, rangeTo: end },
|
||||
});
|
||||
|
||||
const apiToIntercept = {
|
||||
endpoint:
|
||||
'/internal/apm/services/synth-dotnet/transactions/charts/coldstart_rate?*',
|
||||
name: 'coldStartRequest',
|
||||
};
|
||||
|
||||
describe('Service overview - azure functions', () => {
|
||||
before(() => {
|
||||
synthtrace.index(
|
||||
generateData({
|
||||
start: new Date(start).getTime(),
|
||||
end: new Date(end).getTime(),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
synthtrace.clean();
|
||||
});
|
||||
|
||||
it('displays a cold start rate chart and not a transaction breakdown chart', () => {
|
||||
const { endpoint, name } = apiToIntercept;
|
||||
cy.intercept('GET', endpoint).as(name);
|
||||
|
||||
cy.loginAsViewerUser();
|
||||
cy.visitKibana(serviceOverviewHref);
|
||||
cy.wait(`@${name}`);
|
||||
|
||||
cy.contains('Cold start rate');
|
||||
cy.contains('Time spent by span type').should('not.exist');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||
|
||||
const dataConfig = {
|
||||
serviceName: 'synth-dotnet',
|
||||
rate: 10,
|
||||
transaction: {
|
||||
name: 'GET /apple 🍎',
|
||||
duration: 1000,
|
||||
},
|
||||
};
|
||||
|
||||
export function generateData({ start, end }: { start: number; end: number }) {
|
||||
const { rate, transaction, serviceName } = dataConfig;
|
||||
const instance = apm
|
||||
.service({
|
||||
name: serviceName,
|
||||
environment: 'production',
|
||||
agentName: 'dotnet',
|
||||
})
|
||||
.instance('instance-a');
|
||||
|
||||
const traceEvents = timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(rate)
|
||||
.generator((timestamp) =>
|
||||
instance
|
||||
.transaction({ transactionName: transaction.name })
|
||||
.defaults({
|
||||
'service.runtime.name': 'dotnet-isolated',
|
||||
'cloud.provider': 'azure',
|
||||
'cloud.service.name': 'functions',
|
||||
'faas.coldstart': true,
|
||||
})
|
||||
.timestamp(timestamp)
|
||||
.duration(transaction.duration)
|
||||
.success()
|
||||
);
|
||||
|
||||
return traceEvents;
|
||||
}
|
|
@ -9,7 +9,7 @@ import React from 'react';
|
|||
import {
|
||||
isJavaAgentName,
|
||||
isJRubyAgent,
|
||||
isServerlessAgent,
|
||||
isAWSLambdaAgent,
|
||||
} from '../../../../common/agent_name';
|
||||
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
|
||||
import { ServerlessMetrics } from './serverless_metrics';
|
||||
|
@ -17,17 +17,17 @@ import { ServiceMetrics } from './service_metrics';
|
|||
import { JvmMetricsOverview } from './jvm_metrics_overview';
|
||||
|
||||
export function Metrics() {
|
||||
const { agentName, runtimeName } = useApmServiceContext();
|
||||
const isServerless = isServerlessAgent(runtimeName);
|
||||
const { agentName, runtimeName, serverlessType } = useApmServiceContext();
|
||||
const isAWSLambda = isAWSLambdaAgent(serverlessType);
|
||||
|
||||
if (
|
||||
!isServerless &&
|
||||
!isAWSLambda &&
|
||||
(isJavaAgentName(agentName) || isJRubyAgent(agentName, runtimeName))
|
||||
) {
|
||||
return <JvmMetricsOverview />;
|
||||
}
|
||||
|
||||
if (isServerless) {
|
||||
if (isAWSLambda) {
|
||||
return <ServerlessMetrics />;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { isServerlessAgent } from '../../../../common/agent_name';
|
||||
import { isAWSLambdaAgent } from '../../../../common/agent_name';
|
||||
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { ServerlessMetricsDetails } from './serverless_metrics_details';
|
||||
|
@ -15,9 +15,9 @@ export function MetricsDetails() {
|
|||
const {
|
||||
path: { id },
|
||||
} = useApmParams('/services/{serviceName}/metrics/{id}');
|
||||
const { runtimeName } = useApmServiceContext();
|
||||
const { serverlessType } = useApmServiceContext();
|
||||
|
||||
if (isServerlessAgent(runtimeName)) {
|
||||
if (isAWSLambdaAgent(serverlessType)) {
|
||||
return <ServerlessMetricsDetails serverlessId={id} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ export const chartHeight = 288;
|
|||
|
||||
export function ServiceOverview() {
|
||||
const router = useApmRouter();
|
||||
const { serviceName, fallbackToTransactions, agentName, runtimeName } =
|
||||
const { serviceName, fallbackToTransactions, agentName, serverlessType } =
|
||||
useApmServiceContext();
|
||||
|
||||
const {
|
||||
|
@ -54,7 +54,7 @@ export function ServiceOverview() {
|
|||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
const isRumAgent = isRumAgentName(agentName);
|
||||
const isServerless = isServerlessAgent(runtimeName);
|
||||
const isServerless = isServerlessAgent(serverlessType);
|
||||
|
||||
const dependenciesLink = router.link('/services/{serviceName}/dependencies', {
|
||||
path: {
|
||||
|
|
|
@ -35,7 +35,7 @@ export function TransactionDetails() {
|
|||
} = query;
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
const apmRouter = useApmRouter();
|
||||
const { transactionType, fallbackToTransactions, runtimeName } =
|
||||
const { transactionType, fallbackToTransactions, serverlessType } =
|
||||
useApmServiceContext();
|
||||
|
||||
const history = useHistory();
|
||||
|
@ -56,7 +56,7 @@ export function TransactionDetails() {
|
|||
[apmRouter, path, query, transactionName]
|
||||
);
|
||||
|
||||
const isServerless = isServerlessAgent(runtimeName);
|
||||
const isServerless = isServerlessAgent(serverlessType);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -32,7 +32,7 @@ export function TransactionOverview() {
|
|||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const { transactionType, fallbackToTransactions, runtimeName } =
|
||||
const { transactionType, fallbackToTransactions, serverlessType } =
|
||||
useApmServiceContext();
|
||||
|
||||
const history = useHistory();
|
||||
|
@ -42,7 +42,7 @@ export function TransactionOverview() {
|
|||
replace(history, { query: { transactionType } });
|
||||
}
|
||||
|
||||
const isServerless = isServerlessAgent(runtimeName);
|
||||
const isServerless = isServerlessAgent(serverlessType);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -56,6 +56,7 @@ function setup({
|
|||
.mockReturnValue({
|
||||
agentName: 'nodejs',
|
||||
runtimeName: 'node',
|
||||
serverlessType: undefined,
|
||||
error: undefined,
|
||||
status: useFetcherHook.FETCH_STATUS.SUCCESS,
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { isMetricsTabHidden, isInfraTabHidden } from '.';
|
||||
import { ServerlessType } from '../../../../../common/serverless';
|
||||
|
||||
describe('APM service template', () => {
|
||||
describe('isMetricsTabHidden', () => {
|
||||
|
@ -14,7 +15,8 @@ describe('APM service template', () => {
|
|||
{ agentName: 'js-base' },
|
||||
{ agentName: 'rum-js' },
|
||||
{ agentName: 'opentelemetry/webjs' },
|
||||
{ runtimeName: 'aws_lambda' },
|
||||
{ serverlessType: ServerlessType.AWS_LAMBDA },
|
||||
{ serverlessType: ServerlessType.AZURE_FUNCTIONS },
|
||||
].map((input) => {
|
||||
it(`when input ${JSON.stringify(input)}`, () => {
|
||||
expect(isMetricsTabHidden(input)).toBeTruthy();
|
||||
|
@ -47,8 +49,8 @@ describe('APM service template', () => {
|
|||
{ agentName: 'js-base' },
|
||||
{ agentName: 'rum-js' },
|
||||
{ agentName: 'opentelemetry/webjs' },
|
||||
|
||||
{ runtimeName: 'aws_lambda' },
|
||||
{ serverlessType: ServerlessType.AWS_LAMBDA },
|
||||
{ serverlessType: ServerlessType.AZURE_FUNCTIONS },
|
||||
].map((input) => {
|
||||
it(`when input ${JSON.stringify(input)}`, () => {
|
||||
expect(isInfraTabHidden(input)).toBeTruthy();
|
||||
|
|
|
@ -23,6 +23,8 @@ import { useHistory } from 'react-router-dom';
|
|||
import {
|
||||
isMobileAgentName,
|
||||
isRumAgentName,
|
||||
isAWSLambdaAgent,
|
||||
isAzureFunctionsAgent,
|
||||
isServerlessAgent,
|
||||
} from '../../../../../common/agent_name';
|
||||
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
|
||||
|
@ -42,6 +44,7 @@ import { ServiceIcons } from '../../../shared/service_icons';
|
|||
import { TechnicalPreviewBadge } from '../../../shared/technical_preview_badge';
|
||||
import { ApmMainTemplate } from '../apm_main_template';
|
||||
import { AnalyzeDataButton } from './analyze_data_button';
|
||||
import { ServerlessType } from '../../../../../common/serverless';
|
||||
|
||||
type Tab = NonNullable<EuiPageHeaderProps['tabs']>[0] & {
|
||||
key:
|
||||
|
@ -167,33 +170,37 @@ function TemplateWithContext({
|
|||
|
||||
export function isMetricsTabHidden({
|
||||
agentName,
|
||||
runtimeName,
|
||||
serverlessType,
|
||||
isAwsLambdaEnabled,
|
||||
}: {
|
||||
agentName?: string;
|
||||
runtimeName?: string;
|
||||
serverlessType?: ServerlessType;
|
||||
isAwsLambdaEnabled?: boolean;
|
||||
}) {
|
||||
if (isServerlessAgent(runtimeName)) {
|
||||
if (isAWSLambdaAgent(serverlessType)) {
|
||||
return !isAwsLambdaEnabled;
|
||||
}
|
||||
return !agentName || isRumAgentName(agentName);
|
||||
return (
|
||||
!agentName ||
|
||||
isRumAgentName(agentName) ||
|
||||
isAzureFunctionsAgent(serverlessType)
|
||||
);
|
||||
}
|
||||
|
||||
export function isInfraTabHidden({
|
||||
agentName,
|
||||
runtimeName,
|
||||
serverlessType,
|
||||
}: {
|
||||
agentName?: string;
|
||||
runtimeName?: string;
|
||||
serverlessType?: ServerlessType;
|
||||
}) {
|
||||
return (
|
||||
!agentName || isRumAgentName(agentName) || isServerlessAgent(runtimeName)
|
||||
!agentName || isRumAgentName(agentName) || isServerlessAgent(serverlessType)
|
||||
);
|
||||
}
|
||||
|
||||
function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) {
|
||||
const { agentName, runtimeName } = useApmServiceContext();
|
||||
const { agentName, serverlessType } = useApmServiceContext();
|
||||
const { core, plugins } = useApmPluginContext();
|
||||
const { capabilities } = core.application;
|
||||
const { isAlertingAvailable, canReadAlerts } = getAlertingCapabilities(
|
||||
|
@ -288,12 +295,12 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) {
|
|||
label: i18n.translate('xpack.apm.serviceDetails.metricsTabLabel', {
|
||||
defaultMessage: 'Metrics',
|
||||
}),
|
||||
append: isServerlessAgent(runtimeName) && (
|
||||
append: isServerlessAgent(serverlessType) && (
|
||||
<TechnicalPreviewBadge icon="beaker" />
|
||||
),
|
||||
hidden: isMetricsTabHidden({
|
||||
agentName,
|
||||
runtimeName,
|
||||
serverlessType,
|
||||
isAwsLambdaEnabled,
|
||||
}),
|
||||
},
|
||||
|
@ -307,7 +314,7 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) {
|
|||
label: i18n.translate('xpack.apm.home.infraTabLabel', {
|
||||
defaultMessage: 'Infrastructure',
|
||||
}),
|
||||
hidden: isInfraTabHidden({ agentName, runtimeName }),
|
||||
hidden: isInfraTabHidden({ agentName, serverlessType }),
|
||||
},
|
||||
{
|
||||
key: 'service-map',
|
||||
|
@ -328,10 +335,13 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) {
|
|||
label: i18n.translate('xpack.apm.home.serviceLogsTabLabel', {
|
||||
defaultMessage: 'Logs',
|
||||
}),
|
||||
append: isServerlessAgent(runtimeName) && (
|
||||
append: isServerlessAgent(serverlessType) && (
|
||||
<TechnicalPreviewBadge icon="beaker" />
|
||||
),
|
||||
hidden: !agentName || isRumAgentName(agentName),
|
||||
hidden:
|
||||
!agentName ||
|
||||
isRumAgentName(agentName) ||
|
||||
isAzureFunctionsAgent(serverlessType),
|
||||
},
|
||||
{
|
||||
key: 'alerts',
|
||||
|
|
|
@ -20,7 +20,6 @@ import goIcon from './icons/go.svg';
|
|||
import iosIcon from './icons/ios.svg';
|
||||
import darkIosIcon from './icons/ios_dark.svg';
|
||||
import javaIcon from './icons/java.svg';
|
||||
import lambdaIcon from './icons/lambda.svg';
|
||||
import nodeJsIcon from './icons/nodejs.svg';
|
||||
import ocamlIcon from './icons/ocaml.svg';
|
||||
import openTelemetryIcon from './icons/opentelemetry.svg';
|
||||
|
@ -40,7 +39,6 @@ const agentIcons: { [key: string]: string } = {
|
|||
go: goIcon,
|
||||
ios: iosIcon,
|
||||
java: javaIcon,
|
||||
lambda: lambdaIcon,
|
||||
nodejs: nodeJsIcon,
|
||||
ocaml: ocamlIcon,
|
||||
opentelemetry: openTelemetryIcon,
|
||||
|
|
|
@ -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 defaultIcon from '../span_icon/icons/default.svg';
|
||||
import lambdaIcon from './icons/lambda.svg';
|
||||
import azureFunctionsIcon from './icons/functions.svg';
|
||||
import { ServerlessType } from '../../../../common/serverless';
|
||||
|
||||
type ServerlessIcons = Record<ServerlessType, string>;
|
||||
|
||||
const serverlessIcons: ServerlessIcons = {
|
||||
'aws.lambda': lambdaIcon,
|
||||
'azure.functions': azureFunctionsIcon,
|
||||
};
|
||||
|
||||
export function getServerlessIcon(serverlessType?: ServerlessType) {
|
||||
if (!serverlessType) {
|
||||
return defaultIcon;
|
||||
}
|
||||
return serverlessIcons[serverlessType] ?? defaultIcon;
|
||||
}
|
|
@ -189,8 +189,8 @@ describe('ServiceIcons', () => {
|
|||
data: {
|
||||
agentName: 'java',
|
||||
containerType: 'Kubernetes',
|
||||
serverlessType: 'lambda',
|
||||
cloudProvider: 'gcp',
|
||||
serverlessType: 'aws.lambda',
|
||||
cloudProvider: 'aws',
|
||||
},
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
|
@ -234,7 +234,7 @@ describe('ServiceIcons', () => {
|
|||
agentName: 'java',
|
||||
containerType: 'Kubernetes',
|
||||
serverlessType: '',
|
||||
cloudProvider: 'gcp',
|
||||
cloudProvider: 'aws',
|
||||
},
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
|
@ -279,8 +279,8 @@ describe('ServiceIcons', () => {
|
|||
data: {
|
||||
agentName: 'java',
|
||||
containerType: 'Kubernetes',
|
||||
serverlessType: 'lambda',
|
||||
cloudProvider: 'gcp',
|
||||
serverlessType: 'aws.lambda',
|
||||
cloudProvider: 'aws',
|
||||
},
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
|
@ -320,9 +320,9 @@ describe('ServiceIcons', () => {
|
|||
expect(getByTestId('serverless')).toBeInTheDocument();
|
||||
expect(getByTestId('cloud')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(getByTestId('popover_Serverless'));
|
||||
fireEvent.click(getByTestId('popover_AWS Lambda'));
|
||||
expect(queryAllByTestId('loading-content')).toHaveLength(0);
|
||||
expect(getByText('Serverless')).toBeInTheDocument();
|
||||
expect(getByText('AWS Lambda')).toBeInTheDocument();
|
||||
expect(getByText('lambda-java-dev')).toBeInTheDocument();
|
||||
expect(getByText('datasource')).toBeInTheDocument();
|
||||
expect(getByText('http')).toBeInTheDocument();
|
||||
|
@ -334,8 +334,8 @@ describe('ServiceIcons', () => {
|
|||
data: {
|
||||
agentName: 'java',
|
||||
containerType: 'Kubernetes',
|
||||
serverlessType: 'lambda',
|
||||
cloudProvider: 'gcp',
|
||||
serverlessType: 'aws.lambda',
|
||||
cloudProvider: 'aws',
|
||||
},
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
|
|
|
@ -12,11 +12,13 @@ import { useTheme } from '../../../hooks/use_theme';
|
|||
import { ContainerType } from '../../../../common/service_metadata';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { getAgentIcon } from '../agent_icon/get_agent_icon';
|
||||
import { getServerlessIcon } from '../agent_icon/get_serverless_icon';
|
||||
import { CloudDetails } from './cloud_details';
|
||||
import { ServerlessDetails } from './serverless_details';
|
||||
import { ContainerDetails } from './container_details';
|
||||
import { IconPopover } from './icon_popover';
|
||||
import { ServiceDetails } from './service_details';
|
||||
import { ServerlessType } from '../../../../common/serverless';
|
||||
|
||||
interface Props {
|
||||
serviceName: string;
|
||||
|
@ -30,6 +32,26 @@ const cloudIcons: Record<string, string> = {
|
|||
azure: 'logoAzure',
|
||||
};
|
||||
|
||||
function getServerlessTitle(serverlessType?: ServerlessType): string {
|
||||
switch (serverlessType) {
|
||||
case ServerlessType.AWS_LAMBDA: {
|
||||
return i18n.translate('xpack.apm.serviceIcons.aws_lambda', {
|
||||
defaultMessage: 'AWS Lambda',
|
||||
});
|
||||
}
|
||||
case ServerlessType.AZURE_FUNCTIONS: {
|
||||
return i18n.translate('xpack.apm.serviceIcons.azure_functions', {
|
||||
defaultMessage: 'Azure Functions',
|
||||
});
|
||||
}
|
||||
default: {
|
||||
return i18n.translate('xpack.apm.serviceIcons.serverless', {
|
||||
defaultMessage: 'Serverless',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getCloudIcon(provider?: string) {
|
||||
if (provider) {
|
||||
return cloudIcons[provider];
|
||||
|
@ -139,12 +161,10 @@ export function ServiceIcons({ start, end, serviceName }: Props) {
|
|||
{
|
||||
key: 'serverless',
|
||||
icon: {
|
||||
type: getAgentIcon(icons?.serverlessType, theme.darkMode) || 'node',
|
||||
type: getServerlessIcon(icons?.serverlessType) || 'node',
|
||||
},
|
||||
isVisible: !!icons?.serverlessType,
|
||||
title: i18n.translate('xpack.apm.serviceIcons.serverless', {
|
||||
defaultMessage: 'Serverless',
|
||||
}),
|
||||
title: getServerlessTitle(icons?.serverlessType),
|
||||
component: <ServerlessDetails serverless={details?.serverless} />,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -20,10 +20,12 @@ import { useTimeRange } from '../../hooks/use_time_range';
|
|||
import { useFallbackToTransactionsFetcher } from '../../hooks/use_fallback_to_transactions_fetcher';
|
||||
import { replace } from '../../components/shared/links/url_helpers';
|
||||
import { FETCH_STATUS } from '../../hooks/use_fetcher';
|
||||
import { ServerlessType } from '../../../common/serverless';
|
||||
|
||||
export interface APMServiceContextValue {
|
||||
serviceName: string;
|
||||
agentName?: string;
|
||||
serverlessType?: ServerlessType;
|
||||
transactionType?: string;
|
||||
transactionTypes: string[];
|
||||
runtimeName?: string;
|
||||
|
@ -59,6 +61,7 @@ export function ApmServiceContextProvider({
|
|||
const {
|
||||
agentName,
|
||||
runtimeName,
|
||||
serverlessType,
|
||||
status: serviceAgentStatus,
|
||||
} = useServiceAgentFetcher({
|
||||
serviceName,
|
||||
|
@ -88,6 +91,7 @@ export function ApmServiceContextProvider({
|
|||
value={{
|
||||
serviceName,
|
||||
agentName,
|
||||
serverlessType,
|
||||
transactionType: currentTransactionType,
|
||||
transactionTypes,
|
||||
runtimeName,
|
||||
|
|
|
@ -10,6 +10,7 @@ import { useFetcher } from '../../hooks/use_fetcher';
|
|||
const INITIAL_STATE = {
|
||||
agentName: undefined,
|
||||
runtimeName: undefined,
|
||||
serverlessType: undefined,
|
||||
};
|
||||
|
||||
export function useServiceAgentFetcher({
|
||||
|
|
|
@ -30,6 +30,8 @@ Object {
|
|||
"_source": Array [
|
||||
"agent.name",
|
||||
"service.runtime.name",
|
||||
"cloud.provider",
|
||||
"cloud.service.name",
|
||||
],
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
|
@ -54,11 +56,23 @@ Object {
|
|||
},
|
||||
},
|
||||
],
|
||||
"should": Object {
|
||||
"exists": Object {
|
||||
"field": "service.runtime.name",
|
||||
"should": Array [
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "service.runtime.name",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "cloud.provider",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"exists": Object {
|
||||
"field": "cloud.service.name",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"size": 1,
|
||||
|
|
|
@ -11,8 +11,11 @@ import {
|
|||
AGENT_NAME,
|
||||
SERVICE_NAME,
|
||||
SERVICE_RUNTIME_NAME,
|
||||
CLOUD_PROVIDER,
|
||||
CLOUD_SERVICE_NAME,
|
||||
} from '../../../common/es_fields/apm';
|
||||
import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import { getServerlessTypeFromCloudData } from '../../../common/serverless';
|
||||
|
||||
interface ServiceAgent {
|
||||
agent?: {
|
||||
|
@ -23,6 +26,12 @@ interface ServiceAgent {
|
|||
name?: string;
|
||||
};
|
||||
};
|
||||
cloud?: {
|
||||
provider?: string;
|
||||
service?: {
|
||||
name?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export async function getServiceAgent({
|
||||
|
@ -48,7 +57,12 @@ export async function getServiceAgent({
|
|||
body: {
|
||||
track_total_hits: 1,
|
||||
size: 1,
|
||||
_source: [AGENT_NAME, SERVICE_RUNTIME_NAME],
|
||||
_source: [
|
||||
AGENT_NAME,
|
||||
SERVICE_RUNTIME_NAME,
|
||||
CLOUD_PROVIDER,
|
||||
CLOUD_SERVICE_NAME,
|
||||
],
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
|
@ -60,11 +74,23 @@ export async function getServiceAgent({
|
|||
},
|
||||
},
|
||||
],
|
||||
should: {
|
||||
exists: {
|
||||
field: SERVICE_RUNTIME_NAME,
|
||||
should: [
|
||||
{
|
||||
exists: {
|
||||
field: SERVICE_RUNTIME_NAME,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
exists: {
|
||||
field: CLOUD_PROVIDER,
|
||||
},
|
||||
},
|
||||
{
|
||||
exists: {
|
||||
field: CLOUD_SERVICE_NAME,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
sort: {
|
||||
|
@ -81,6 +107,16 @@ export async function getServiceAgent({
|
|||
return {};
|
||||
}
|
||||
|
||||
const { agent, service } = response.hits.hits[0]._source as ServiceAgent;
|
||||
return { agentName: agent?.name, runtimeName: service?.runtime?.name };
|
||||
const { agent, service, cloud } = response.hits.hits[0]
|
||||
._source as ServiceAgent;
|
||||
const serverlessType = getServerlessTypeFromCloudData(
|
||||
cloud?.provider,
|
||||
cloud?.service?.name
|
||||
);
|
||||
|
||||
return {
|
||||
agentName: agent?.name,
|
||||
runtimeName: service?.runtime?.name,
|
||||
serverlessType,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -21,6 +21,10 @@ import { ContainerType } from '../../../common/service_metadata';
|
|||
import { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw';
|
||||
import { getProcessorEventForTransactions } from '../../lib/helpers/transactions';
|
||||
import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import {
|
||||
ServerlessType,
|
||||
getServerlessTypeFromCloudData,
|
||||
} from '../../../common/serverless';
|
||||
|
||||
type ServiceMetadataIconsRaw = Pick<
|
||||
TransactionRaw,
|
||||
|
@ -30,7 +34,7 @@ type ServiceMetadataIconsRaw = Pick<
|
|||
export interface ServiceMetadataIcons {
|
||||
agentName?: string;
|
||||
containerType?: ContainerType;
|
||||
serverlessType?: string;
|
||||
serverlessType?: ServerlessType;
|
||||
cloudProvider?: string;
|
||||
}
|
||||
|
||||
|
@ -106,10 +110,10 @@ export async function getServiceMetadataIcons({
|
|||
containerType = 'Docker';
|
||||
}
|
||||
|
||||
let serverlessType: string | undefined;
|
||||
if (cloud?.provider === 'aws' && cloud?.service?.name === 'lambda') {
|
||||
serverlessType = 'lambda';
|
||||
}
|
||||
const serverlessType = getServerlessTypeFromCloudData(
|
||||
cloud?.provider,
|
||||
cloud?.service?.name
|
||||
);
|
||||
|
||||
return {
|
||||
agentName: agent?.name,
|
||||
|
|
|
@ -58,6 +58,7 @@ import { createInfraMetricsClient } from '../../lib/helpers/create_es_client/cre
|
|||
import { getApmEventClient } from '../../lib/helpers/get_apm_event_client';
|
||||
import { getApmAlertsClient } from '../../lib/helpers/get_apm_alerts_client';
|
||||
import { getServicesAlerts } from './get_services/get_service_alerts';
|
||||
import { ServerlessType } from '../../../common/serverless';
|
||||
|
||||
const servicesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/services',
|
||||
|
@ -364,10 +365,11 @@ const serviceAgentRoute = createApmServerRoute({
|
|||
options: { tags: ['access:apm'] },
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<
|
||||
| { agentName?: undefined; runtimeName?: undefined }
|
||||
| { agentName: string | undefined; runtimeName: string | undefined }
|
||||
> => {
|
||||
): Promise<{
|
||||
agentName?: string;
|
||||
runtimeName?: string;
|
||||
serverlessType?: ServerlessType;
|
||||
}> => {
|
||||
const apmEventClient = await getApmEventClient(resources);
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import { getServerlessTypeFromCloudData } from '@kbn/apm-plugin/common/serverless';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { dataConfig, generateData } from './generate_data';
|
||||
|
||||
|
@ -62,12 +63,14 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
|
||||
it('returns correct metadata', () => {
|
||||
const { agentName, cloud } = dataConfig;
|
||||
const { provider, serviceName: cloudServiceName } = cloud;
|
||||
const { provider, serviceName: cloudServiceName, provider: cloudProvider } = cloud;
|
||||
|
||||
expect(body.agentName).to.be(agentName);
|
||||
expect(body.cloudProvider).to.be(provider);
|
||||
expect(body.containerType).to.be('Kubernetes');
|
||||
expect(body.serverlessType).to.be(cloudServiceName);
|
||||
expect(body.serverlessType).to.be(
|
||||
getServerlessTypeFromCloudData(cloudProvider, cloudServiceName)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue