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:
Alexander Wert 2023-01-27 09:52:38 +01:00 committed by GitHub
parent 5c97c487e8
commit 7ee827b844
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 452 additions and 82 deletions

View file

@ -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,

View file

@ -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 });

View 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;

View file

@ -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);
});
});
});

View file

@ -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) {

View file

@ -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;
}
}

View file

@ -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)

View file

@ -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');
});
});

View file

@ -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;
}

View file

@ -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 />;
}

View file

@ -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} />;
}

View file

@ -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: {

View file

@ -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 (
<>

View file

@ -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 (
<>

View file

@ -56,6 +56,7 @@ function setup({
.mockReturnValue({
agentName: 'nodejs',
runtimeName: 'node',
serverlessType: undefined,
error: undefined,
status: useFetcherHook.FETCH_STATUS.SUCCESS,
});

View file

@ -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();

View file

@ -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',

View file

@ -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,

View file

@ -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;
}

View file

@ -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(),

View file

@ -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} />,
},
{

View file

@ -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,

View file

@ -10,6 +10,7 @@ import { useFetcher } from '../../hooks/use_fetcher';
const INITIAL_STATE = {
agentName: undefined,
runtimeName: undefined,
serverlessType: undefined,
};
export function useServiceAgentFetcher({

View file

@ -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,

View file

@ -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,
};
}

View file

@ -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,

View file

@ -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;

View file

@ -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)
);
});
});
}