mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[APM][OTel] Add url.full fallback (#215397)
## Summary This PR fixes the missing URL in the transaction summary ## Testing [_UPDATED_] - [SOLVED ✅ ⬇️ ] ~~This is tricky to test ( I am trying to create a serverless instance from this PR and it should make it easier)~~ - Testing on serverless (the env linked in the PR) - EDOT service (I run locally in Docker and connect to the env): <img width="1904" alt="image" src="https://github.com/user-attachments/assets/c3a7ab56-5b8f-42a5-8033-55ccbb915b40" /> - Other generated service (from the env):  - In the meantime - using synthtrace: Case to run/expectation - `node scripts/synthtrace otel_edot_simple_trace.ts` / The trace summary should be visible  - `node scripts/synthtrace simple_trace.ts` / The trace summary should still be visible (using `url.full` in this case) 
This commit is contained in:
parent
c5e0b05454
commit
a987209d3f
17 changed files with 248 additions and 11 deletions
|
@ -144,6 +144,8 @@ class OtelEdot extends Serializable<OtelEdotDocument> {
|
|||
'app.ads.count': 1,
|
||||
'event.outcome': 'success',
|
||||
'event.success_count': 1,
|
||||
'http.request.method': 'GET',
|
||||
'http.response.status_code': 200,
|
||||
'network.peer.address': '10.12.9.56',
|
||||
'network.peer.port': 41208,
|
||||
'network.type': 'ipv4',
|
||||
|
@ -166,6 +168,8 @@ class OtelEdot extends Serializable<OtelEdotDocument> {
|
|||
'transaction.root': false,
|
||||
'transaction.sampled': true,
|
||||
'transaction.type': 'request',
|
||||
'url.path': '/some/path',
|
||||
'url.scheme': 'https',
|
||||
},
|
||||
data_stream: {
|
||||
dataset: 'generic.otel',
|
||||
|
|
|
@ -51,6 +51,8 @@ export interface OtelEdotTransactionDocument extends OtelEdotDocument {
|
|||
'session.id'?: string;
|
||||
'thread.id'?: number;
|
||||
'thread.name'?: string;
|
||||
'url.path'?: string;
|
||||
'url.scheme'?: string;
|
||||
};
|
||||
status?: {
|
||||
code?: string;
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -9,4 +9,6 @@ export interface Url {
|
|||
domain?: string;
|
||||
full?: string;
|
||||
original?: string;
|
||||
scheme?: string;
|
||||
path?: string;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"`;
|
||||
|
|
|
@ -20,6 +20,12 @@ const baseUrl = url.format({
|
|||
query: { rangeFrom: start, rangeTo: end },
|
||||
});
|
||||
|
||||
const transactionTabPath = '/app/apm/services/adservice-edot-synth/transactions/view';
|
||||
const transactionUrl = url.format({
|
||||
pathname: transactionTabPath,
|
||||
query: { rangeFrom: start, rangeTo: end, transactionName: 'oteldemo.AdServiceEdotSynth/GetAds' },
|
||||
});
|
||||
|
||||
describe('Service Overview', () => {
|
||||
before(() => {
|
||||
synthtraceOtel.index(
|
||||
|
@ -98,6 +104,19 @@ describe('Service Overview', () => {
|
|||
cy.contains('a', 'oteldemo.AdServiceEdotSynth/GetAds').click();
|
||||
cy.contains('h5', 'oteldemo.AdServiceEdotSynth/GetAds');
|
||||
});
|
||||
it('shows transaction summary', () => {
|
||||
cy.visitKibana(transactionUrl);
|
||||
|
||||
cy.getByTestSubj('apmHttpInfoRequestMethod').should('exist');
|
||||
cy.getByTestSubj('apmHttpInfoRequestMethod').contains('GET');
|
||||
cy.getByTestSubj('apmHttpInfoUrl').should('exist');
|
||||
cy.getByTestSubj('apmHttpInfoUrl').contains(
|
||||
'https://otel-demo-blue-adservice-edot-synth:8080/some/path'
|
||||
);
|
||||
cy.getByTestSubj('apmHttpInfoRequestMethod').should('exist');
|
||||
cy.getByTestSubj('apmHttpStatusBadge').should('exist');
|
||||
cy.getByTestSubj('apmHttpStatusBadge').contains('OK');
|
||||
});
|
||||
});
|
||||
|
||||
describe('errors', () => {
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -33,6 +33,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 {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue