[9.0] [APM][OTel] Add url.full fallback (#215397) (#216635)

# Backport

This will backport the following commits from `main` to `9.0`:
- [[APM][OTel] Add url.full fallback
(#215397)](https://github.com/elastic/kibana/pull/215397)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT
[{"author":{"name":"jennypavlova","email":"dzheni.pavlova@elastic.co"},"sourceCommit":{"committedDate":"2025-04-01T09:50:28Z","message":"[APM][OTel]
Add url.full fallback (#215397)\n\n## Summary\n\nThis PR fixes the
missing URL in the transaction summary \n\n## Testing [_UPDATED_]\n-
[SOLVED  ⬇️ ] ~~This is tricky to test ( I am trying to create
a\nserverless instance from this PR and it should make it easier)~~\n-
Testing on serverless (the env linked in the PR) \n - EDOT service (I
run locally in Docker and connect to the env): \n<img width=\"1904\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/c3a7ab56-5b8f-42a5-8033-55ccbb915b40\"\n/>\n\n
- Other generated service (from the
env):\n\n![image](https://github.com/user-attachments/assets/e76f2901-050a-4ee3-b536-a057d45454e2)\n
\n- In the meantime \n - using synthtrace: Case to run/expectation\n-
`node scripts/synthtrace otel_edot_simple_trace.ts` / The trace\nsummary
should be visible\n
\n\n![image](https://github.com/user-attachments/assets/b9ad8a8b-f89a-449c-a053-a5628c2fa620)\n\n-
`node scripts/synthtrace simple_trace.ts` / The trace summary
should\nstill be visible (using `url.full` in this case)\n
\n\n![image](https://github.com/user-attachments/assets/93ffac41-9f79-4d09-ab69-ae5c8e782750)","sha":"a987209d3fc6c10153181781bdf1c8c829ceba04","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","backport:prev-minor","backport:prev-major","ci:build-serverless-image","ci:project-deploy-observability","Team:obs-ux-infra_services","ci:project-redeploy","v9.1.0"],"title":"[APM][OTel]
Add url.full
fallback","number":215397,"url":"https://github.com/elastic/kibana/pull/215397","mergeCommit":{"message":"[APM][OTel]
Add url.full fallback (#215397)\n\n## Summary\n\nThis PR fixes the
missing URL in the transaction summary \n\n## Testing [_UPDATED_]\n-
[SOLVED  ⬇️ ] ~~This is tricky to test ( I am trying to create
a\nserverless instance from this PR and it should make it easier)~~\n-
Testing on serverless (the env linked in the PR) \n - EDOT service (I
run locally in Docker and connect to the env): \n<img width=\"1904\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/c3a7ab56-5b8f-42a5-8033-55ccbb915b40\"\n/>\n\n
- Other generated service (from the
env):\n\n![image](https://github.com/user-attachments/assets/e76f2901-050a-4ee3-b536-a057d45454e2)\n
\n- In the meantime \n - using synthtrace: Case to run/expectation\n-
`node scripts/synthtrace otel_edot_simple_trace.ts` / The trace\nsummary
should be visible\n
\n\n![image](https://github.com/user-attachments/assets/b9ad8a8b-f89a-449c-a053-a5628c2fa620)\n\n-
`node scripts/synthtrace simple_trace.ts` / The trace summary
should\nstill be visible (using `url.full` in this case)\n
\n\n![image](https://github.com/user-attachments/assets/93ffac41-9f79-4d09-ab69-ae5c8e782750)","sha":"a987209d3fc6c10153181781bdf1c8c829ceba04"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/215397","number":215397,"mergeCommit":{"message":"[APM][OTel]
Add url.full fallback (#215397)\n\n## Summary\n\nThis PR fixes the
missing URL in the transaction summary \n\n## Testing [_UPDATED_]\n-
[SOLVED  ⬇️ ] ~~This is tricky to test ( I am trying to create
a\nserverless instance from this PR and it should make it easier)~~\n-
Testing on serverless (the env linked in the PR) \n - EDOT service (I
run locally in Docker and connect to the env): \n<img width=\"1904\"
alt=\"image\"\nsrc=\"https://github.com/user-attachments/assets/c3a7ab56-5b8f-42a5-8033-55ccbb915b40\"\n/>\n\n
- Other generated service (from the
env):\n\n![image](https://github.com/user-attachments/assets/e76f2901-050a-4ee3-b536-a057d45454e2)\n
\n- In the meantime \n - using synthtrace: Case to run/expectation\n-
`node scripts/synthtrace otel_edot_simple_trace.ts` / The trace\nsummary
should be visible\n
\n\n![image](https://github.com/user-attachments/assets/b9ad8a8b-f89a-449c-a053-a5628c2fa620)\n\n-
`node scripts/synthtrace simple_trace.ts` / The trace summary
should\nstill be visible (using `url.full` in this case)\n
\n\n![image](https://github.com/user-attachments/assets/93ffac41-9f79-4d09-ab69-ae5c8e782750)","sha":"a987209d3fc6c10153181781bdf1c8c829ceba04"}}]}]
BACKPORT-->
This commit is contained in:
jennypavlova 2025-04-01 18:27:14 +02:00 committed by GitHub
parent b587f643d0
commit 29998ee927
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 223 additions and 11 deletions

View file

@ -189,13 +189,19 @@ export const METRIC_OTEL_JVM_SYSTEM_CPU_PERCENT = 'process.runtime.jvm.system.cp
export const METRIC_OTEL_JVM_GC_DURATION = 'process.runtime.jvm.gc.duration';
export const VALUE_OTEL_JVM_PROCESS_MEMORY_HEAP = 'heap';
export const VALUE_OTEL_JVM_PROCESS_MEMORY_NON_HEAP = 'non_heap';
// OpenTelemetry semconv fields for AgentName https://opentelemetry.io/docs/specs/semconv/resource/#telemetry-sdk
export const TELEMETRY_SDK_NAME = 'telemetry.sdk.name';
export const TELEMETRY_SDK_LANGUAGE = 'telemetry.sdk.language';
export const TELEMETRY_SDK_VERSION = 'telemetry.sdk.version';
// OpenTelemetry span links
// OpenTelemetry semconv fields for HTTP server https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-server-semantic-conventions
export const URL_PATH = 'url.path';
export const URL_SCHEME = 'url.scheme';
export const SERVER_ADDRESS = 'server.address';
export const SERVER_PORT = 'server.port';
// OpenTelemetry span links
export const LINKS_SPAN_ID = 'links.span_id';
export const LINKS_TRACE_ID = 'links.trace_id';

View file

@ -19,6 +19,7 @@ import {
Url,
User,
} from './fields';
import { Server } from './fields/server';
export interface Processor {
name: 'error';
@ -70,5 +71,6 @@ export interface ErrorRaw extends APMBaseDoc {
process?: Process;
service: Service;
url?: Url;
server?: Server;
user?: User;
}

View file

@ -0,0 +1,11 @@
/*
* 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.
*/
export interface Server {
address?: string;
port?: string;
}

View file

@ -9,4 +9,6 @@ export interface Url {
domain?: string;
full?: string;
original?: string;
scheme?: string;
path?: string;
}

View file

@ -8,6 +8,7 @@
import { APMBaseDoc } from './apm_base_doc';
import { EventOutcome } from './fields/event_outcome';
import { Http } from './fields/http';
import { Server } from './fields/server';
import { SpanLink } from './fields/span_links';
import { Stackframe } from './fields/stackframe';
import { TimestampUs } from './fields/timestamp_us';
@ -76,4 +77,5 @@ export interface SpanRaw extends APMBaseDoc {
};
http?: Http;
url?: Url;
server?: Server;
}

View file

@ -21,6 +21,7 @@ import { User } from './fields/user';
import { UserAgent } from './fields/user_agent';
import { Faas } from './fields/faas';
import { SpanLink } from './fields/span_links';
import { Server } from './fields/server';
interface Processor {
name: 'transaction';
@ -64,6 +65,7 @@ export interface TransactionRaw extends APMBaseDoc {
ecs?: { version?: string };
host?: Host;
http?: Http;
server?: Server;
kubernetes?: Kubernetes;
process?: Process;
service: Service;

View file

@ -256,6 +256,10 @@ exports[`Error PROCESSOR_EVENT 1`] = `"error"`;
exports[`Error PROCESSOR_NAME 1`] = `"error"`;
exports[`Error SERVER_ADDRESS 1`] = `undefined`;
exports[`Error SERVER_PORT 1`] = `undefined`;
exports[`Error SERVICE 1`] = `
Object {
"language": Object {
@ -372,6 +376,10 @@ exports[`Error TRANSACTION_TYPE 1`] = `"request"`;
exports[`Error URL_FULL 1`] = `undefined`;
exports[`Error URL_PATH 1`] = `undefined`;
exports[`Error URL_SCHEME 1`] = `undefined`;
exports[`Error USER_AGENT_NAME 1`] = `undefined`;
exports[`Error USER_AGENT_ORIGINAL 1`] = `undefined`;
@ -627,6 +635,10 @@ exports[`Span PROCESSOR_EVENT 1`] = `"span"`;
exports[`Span PROCESSOR_NAME 1`] = `"transaction"`;
exports[`Span SERVER_ADDRESS 1`] = `undefined`;
exports[`Span SERVER_PORT 1`] = `undefined`;
exports[`Span SERVICE 1`] = `
Object {
"name": "service name",
@ -739,6 +751,10 @@ exports[`Span TRANSACTION_TYPE 1`] = `undefined`;
exports[`Span URL_FULL 1`] = `undefined`;
exports[`Span URL_PATH 1`] = `undefined`;
exports[`Span URL_SCHEME 1`] = `undefined`;
exports[`Span USER_AGENT_NAME 1`] = `undefined`;
exports[`Span USER_AGENT_ORIGINAL 1`] = `undefined`;
@ -1008,6 +1024,10 @@ exports[`Transaction PROCESSOR_EVENT 1`] = `"transaction"`;
exports[`Transaction PROCESSOR_NAME 1`] = `"transaction"`;
exports[`Transaction SERVER_ADDRESS 1`] = `undefined`;
exports[`Transaction SERVER_PORT 1`] = `undefined`;
exports[`Transaction SERVICE 1`] = `
Object {
"language": Object {
@ -1124,6 +1144,10 @@ exports[`Transaction TRANSACTION_TYPE 1`] = `"transaction type"`;
exports[`Transaction URL_FULL 1`] = `"http://www.elastic.co"`;
exports[`Transaction URL_PATH 1`] = `undefined`;
exports[`Transaction URL_SCHEME 1`] = `undefined`;
exports[`Transaction USER_AGENT_NAME 1`] = `"Other"`;
exports[`Transaction USER_AGENT_ORIGINAL 1`] = `"test original"`;

View file

@ -64,7 +64,7 @@ export function WaterfallWithSummary<TSample extends {}>({
waterfallFetchStatus === FETCH_STATUS.LOADING ||
traceSamplesFetchStatus === FETCH_STATUS.LOADING;
// When traceId is not present, call to waterfallFetchResult will not be initiated
const isSucceded =
const isSucceeded =
(waterfallFetchStatus === FETCH_STATUS.SUCCESS ||
waterfallFetchStatus === FETCH_STATUS.NOT_INITIATED) &&
traceSamplesFetchStatus === FETCH_STATUS.SUCCESS;
@ -91,7 +91,7 @@ export function WaterfallWithSummary<TSample extends {}>({
const { entryTransaction } = waterfallFetchResult;
if (!entryTransaction && traceSamples?.length === 0 && isSucceded) {
if (!entryTransaction && traceSamples?.length === 0 && isSucceeded) {
return (
<EuiEmptyPrompt
title={

View file

@ -23,7 +23,7 @@ const urlStyles = css`
interface HttpInfoProps {
method?: string;
status?: number;
url: string;
url?: string;
}
export function HttpInfoSummaryItem({ status, method, url }: HttpInfoProps) {
@ -54,11 +54,13 @@ export function HttpInfoSummaryItem({ status, method, url }: HttpInfoProps) {
<span data-test-subj="apmHttpInfoRequestMethod">{method.toUpperCase()}</span>
</EuiToolTip>
)}{' '}
<EuiToolTip content={url}>
<span data-test-subj="apmHttpInfoUrl" css={urlStyles}>
{url}
</span>
</EuiToolTip>
{url && (
<EuiToolTip content={url}>
<span data-test-subj="apmHttpInfoUrl" css={urlStyles}>
{url}
</span>
</EuiToolTip>
)}
</EuiBadge>
{status && <HttpStatusBadge status={status} />}
</span>

View file

@ -15,6 +15,7 @@ import { HttpInfoSummaryItem } from './http_info_summary_item';
import { TransactionResultSummaryItem } from './transaction_result_summary_item';
import { UserAgentSummaryItem } from './user_agent_summary_item';
import { ColdStartBadge } from '../../app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/cold_start_badge';
import { buildUrl } from '../../../utils/build_url';
interface Props {
transaction: Transaction;
@ -25,12 +26,13 @@ interface Props {
function getTransactionResultSummaryItem(transaction: Transaction) {
const result = transaction.transaction.result;
const url = transaction.url?.full || transaction.transaction?.page?.url;
const urlFull = transaction.url?.full || transaction.transaction?.page?.url;
const url = urlFull ?? buildUrl(transaction);
if (url) {
const method = transaction.http?.request?.method;
const status = transaction.http?.response?.status_code;
return <HttpInfoSummaryItem method={method} status={status} url={url} />;
}

View file

@ -0,0 +1,115 @@
/*
* 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 { buildUrl } from './build_url';
import type { Transaction } from '../../typings/es_schemas/ui/transaction';
describe('buildUrl', () => {
it('should return a full URL when all fields are provided', () => {
const item = {
url: {
scheme: 'ftp',
path: '/some/path',
},
server: {
address: 'example.com',
port: 443,
},
};
const result = buildUrl(item as unknown as Transaction);
expect(result).toBe('ftp://example.com:443/some/path');
});
it('should return a URL without a port if the port is not provided', () => {
const item = {
url: {
scheme: 'http',
path: '/another/path',
},
server: {
address: 'example.org',
},
};
const result = buildUrl(item as Transaction);
expect(result).toBe('http://example.org/another/path');
});
it('should return a URL without a path if the path is not provided', () => {
const item = {
url: {
scheme: 'https',
},
server: {
address: 'example.net',
port: 8443,
},
};
const result = buildUrl(item as unknown as Transaction);
expect(result).toBe('https://example.net:8443/');
});
it('should return undefined if the scheme is missing', () => {
const item = {
url: {
path: '/missing/scheme',
},
server: {
address: 'example.com',
port: 8080,
},
};
const result = buildUrl(item as unknown as Transaction);
expect(result).toBeUndefined();
});
it('should return undefined if the server address is missing', () => {
const item = {
url: {
scheme: 'https',
path: '/missing/address',
},
server: {
port: 8080,
},
};
const result = buildUrl(item as unknown as Transaction);
expect(result).toBeUndefined();
});
it('should return undefined and log an error if the port is invalid', () => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const item = {
url: {
scheme: 'https',
path: '/invalid/port',
},
server: {
address: 'example.com',
port: 'invalid-port',
},
};
const result = buildUrl(item as unknown as Transaction);
expect(result).toBeUndefined();
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Failed to build URL',
expect.objectContaining({
message: 'Invalid base URL: https://example.com:invalid-port',
})
);
consoleErrorSpy.mockRestore();
});
it('should handle an empty object gracefully', () => {
const item = {};
const result = buildUrl(item as Transaction);
expect(result).toBeUndefined();
});
});

View file

@ -0,0 +1,32 @@
/*
* 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 { Transaction } from '../../typings/es_schemas/ui/transaction';
import type { Span } from '../../typings/es_schemas/ui/span';
import type { APMError } from '../../typings/es_schemas/ui/apm_error';
export const buildUrl = (item: Transaction | Span | APMError) => {
// URL fields from Otel
const urlScheme = item?.url?.scheme;
const urlPath = item?.url?.path;
const serverAddress = item?.server?.address;
const serverPort = item?.server?.port;
const hasURLFromFields = urlScheme && serverAddress;
const urlServerPort = serverPort ? `:${serverPort}` : '';
try {
const url = hasURLFromFields
? new URL(urlPath ?? '', `${urlScheme}://${serverAddress}${urlServerPort}`).toString()
: undefined;
return url;
} catch (e) {
console.error('Failed to build URL', e);
return undefined;
}
};

View file

@ -34,6 +34,10 @@ Object {
"http.response.status_code",
"http.request.method",
"user_agent.name",
"url.path",
"url.scheme",
"server.address",
"server.port",
"user_agent.version",
],
"query": Object {

View file

@ -30,6 +30,10 @@ import {
HTTP_RESPONSE_STATUS_CODE,
TRANSACTION_PAGE_URL,
USER_AGENT_NAME,
URL_PATH,
URL_SCHEME,
SERVER_ADDRESS,
SERVER_PORT,
USER_AGENT_VERSION,
} from '../../../../common/es_fields/apm';
import { asMutableArray } from '../../../../common/utils/as_mutable_array';
@ -72,6 +76,10 @@ export async function getTransaction({
HTTP_RESPONSE_STATUS_CODE,
HTTP_REQUEST_METHOD,
USER_AGENT_NAME,
URL_PATH,
URL_SCHEME,
SERVER_ADDRESS,
SERVER_PORT,
USER_AGENT_VERSION,
] as const);