diff --git a/.changeset/otel-semconv-exceptions.md b/.changeset/otel-semconv-exceptions.md new file mode 100644 index 0000000000..6712b348af --- /dev/null +++ b/.changeset/otel-semconv-exceptions.md @@ -0,0 +1,12 @@ +--- +'@hyperdx/app': patch +--- + +fix: Show the Exception panel for logs and spans that carry `exception.type`, +`exception.message` or `exception.stacktrace` attributes, not only for +`exception` span events; show correlated logs in the trace waterfall even when +their span was never exported, and color error logs and `exception` span events +red there; and set `spanEventsValueExpression` and `spanLinksValueExpression` on +the default trace sources so span exceptions and links load out of the box. The +default sources also gain the summary and exponential histogram metric tables +and drop key rollup tables the schema no longer creates. diff --git a/docker-compose.yml b/docker-compose.yml index c2d85bd419..bf1a89133f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -95,13 +95,14 @@ services: '[{"name":"Local ClickHouse","host":"http://ch-server:8123","username":"default","password":""}]' DEFAULT_SOURCES: - '[{"from":{"databaseName":"default","tableName":"otel_logs"},"kind":"log","timestampValueExpression":"Timestamp","name":"Logs","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"Body","serviceNameExpression":"ServiceName","bodyExpression":"Body","eventAttributesExpression":"LogAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,SeverityText,Body","severityTextExpression":"SeverityText","traceIdExpression":"TraceId","spanIdExpression":"SpanId","metadataMaterializedViews":{"keyRollupTable":"otel_logs_key_rollup_15m","kvRollupTable":"otel_logs_kv_rollup_15m","granularity":"15 + '[{"from":{"databaseName":"default","tableName":"otel_logs"},"kind":"log","timestampValueExpression":"Timestamp","name":"Logs","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"Body","serviceNameExpression":"ServiceName","bodyExpression":"Body","eventAttributesExpression":"LogAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,SeverityText,Body","severityTextExpression":"SeverityText","traceIdExpression":"TraceId","spanIdExpression":"SpanId","metadataMaterializedViews":{"kvRollupTable":"otel_logs_kv_rollup_15m","granularity":"15 minute"},"connection":"Local - ClickHouse","traceSourceId":"Traces","sessionSourceId":"Sessions","metricSourceId":"Metrics"},{"from":{"databaseName":"default","tableName":"otel_traces"},"kind":"trace","timestampValueExpression":"Timestamp","name":"Traces","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"SpanName","serviceNameExpression":"ServiceName","eventAttributesExpression":"SpanAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,StatusCode,round(Duration/1e6),SpanName","traceIdExpression":"TraceId","spanIdExpression":"SpanId","durationExpression":"Duration","durationPrecision":9,"parentSpanIdExpression":"ParentSpanId","spanNameExpression":"SpanName","spanKindExpression":"SpanKind","statusCodeExpression":"StatusCode","statusMessageExpression":"StatusMessage","metadataMaterializedViews":{"keyRollupTable":"otel_traces_key_rollup_15m","kvRollupTable":"otel_traces_kv_rollup_15m","granularity":"15 + ClickHouse","traceSourceId":"Traces","metricSourceId":"Metrics"},{"from":{"databaseName":"default","tableName":"otel_traces"},"kind":"trace","timestampValueExpression":"Timestamp","name":"Traces","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"SpanName","serviceNameExpression":"ServiceName","eventAttributesExpression":"SpanAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,StatusCode,round(Duration/1e6),SpanName","traceIdExpression":"TraceId","spanIdExpression":"SpanId","durationExpression":"Duration","durationPrecision":9,"parentSpanIdExpression":"ParentSpanId","spanNameExpression":"SpanName","spanKindExpression":"SpanKind","statusCodeExpression":"StatusCode","statusMessageExpression":"StatusMessage","spanEventsValueExpression":"Events","spanLinksValueExpression":"Links","metadataMaterializedViews":{"kvRollupTable":"otel_traces_kv_rollup_15m","granularity":"15 minute"},"connection":"Local - ClickHouse","logSourceId":"Logs","sessionSourceId":"Sessions","metricSourceId":"Metrics"},{"from":{"databaseName":"default","tableName":""},"kind":"metric","timestampValueExpression":"TimeUnix","name":"Metrics","resourceAttributesExpression":"ResourceAttributes","metricTables":{"gauge":"otel_metrics_gauge","histogram":"otel_metrics_histogram","sum":"otel_metrics_sum","_id":"682586a8b1f81924e628e808","id":"682586a8b1f81924e628e808"},"connection":"Local - ClickHouse","logSourceId":"Logs","traceSourceId":"Traces","sessionSourceId":"Sessions"},{"from":{"databaseName":"default","tableName":"hyperdx_sessions"},"kind":"session","timestampValueExpression":"TimestampTime","name":"Sessions","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"Body","serviceNameExpression":"ServiceName","bodyExpression":"Body","eventAttributesExpression":"LogAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,SeverityText,Body","severityTextExpression":"SeverityText","traceIdExpression":"TraceId","spanIdExpression":"SpanId","connection":"Local - ClickHouse","logSourceId":"Logs","traceSourceId":"Traces","metricSourceId":"Metrics"}]' + ClickHouse","logSourceId":"Logs","sessionSourceId":"Sessions","metricSourceId":"Metrics"},{"from":{"databaseName":"default","tableName":""},"kind":"metric","timestampValueExpression":"TimeUnix","name":"Metrics","resourceAttributesExpression":"ResourceAttributes","metricTables":{"gauge":"otel_metrics_gauge","histogram":"otel_metrics_histogram","sum":"otel_metrics_sum","summary":"otel_metrics_summary","exponential + histogram":"otel_metrics_exponential_histogram"},"connection":"Local + ClickHouse","logSourceId":"Logs"},{"from":{"databaseName":"default","tableName":"hyperdx_sessions"},"kind":"session","timestampValueExpression":"TimestampTime","name":"Sessions","resourceAttributesExpression":"ResourceAttributes","connection":"Local + ClickHouse","traceSourceId":"Traces"}]' networks: - internal depends_on: diff --git a/docker/hyperdx/entry.local.base.sh b/docker/hyperdx/entry.local.base.sh index b46fad6b96..2c464a247d 100644 --- a/docker/hyperdx/entry.local.base.sh +++ b/docker/hyperdx/entry.local.base.sh @@ -36,10 +36,10 @@ if [ -z "${DEFAULT_CONNECTIONS}" ]; then DEFAULT_CONNECTIONS='[{"name":"Local ClickHouse","host":"http://localhost:8123","username":"default","password":""}]' fi export DEFAULT_CONNECTIONS -export DEFAULT_SOURCES='[{"from":{"databaseName":"default","tableName":"otel_logs"},"kind":"log","timestampValueExpression":"Timestamp","name":"Logs","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"Body","serviceNameExpression":"ServiceName","bodyExpression":"Body","eventAttributesExpression":"LogAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,SeverityText,Body","severityTextExpression":"SeverityText","traceIdExpression":"TraceId","spanIdExpression":"SpanId","metadataMaterializedViews":{"keyRollupTable":"otel_logs_key_rollup_15m","kvRollupTable":"otel_logs_kv_rollup_15m","granularity":"15 minute"},"connection":"Local ClickHouse","traceSourceId":"Traces","sessionSourceId":"Sessions","metricSourceId":"Metrics"},{"from":{"databaseName":"default","tableName":"otel_traces"},"kind":"trace","timestampValueExpression":"Timestamp","name":"Traces","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"SpanName","serviceNameExpression":"ServiceName","eventAttributesExpression":"SpanAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,StatusCode,round(Duration/1e6),SpanName","traceIdExpression":"TraceId","spanIdExpression":"SpanId","durationExpression":"Duration","durationPrecision":9,"parentSpanIdExpression":"ParentSpanId","spanNameExpression":"SpanName","spanKindExpression":"SpanKind","statusCodeExpression":"StatusCode","statusMessageExpression":"StatusMessage","metadataMaterializedViews":{"keyRollupTable":"otel_traces_key_rollup_15m","kvRollupTable":"otel_traces_kv_rollup_15m","granularity":"15 minute"},"connection":"Local ClickHouse","logSourceId":"Logs","sessionSourceId":"Sessions","metricSourceId":"Metrics"},{"from":{"databaseName":"default","tableName":""},"kind":"metric","timestampValueExpression":"TimeUnix","name":"Metrics","resourceAttributesExpression":"ResourceAttributes","metricTables":{"gauge":"otel_metrics_gauge","histogram":"otel_metrics_histogram","sum":"otel_metrics_sum","summary":"otel_metrics_summary","exponentialHistogram":"otel_metrics_exponential_histogram","_id":"682586a8b1f81924e628e808","id":"682586a8b1f81924e628e808"},"connection":"Local ClickHouse","logSourceId":"Logs","traceSourceId":"Traces","sessionSourceId":"Sessions"},{"from":{"databaseName":"default","tableName":"hyperdx_sessions"},"kind":"session","timestampValueExpression":"TimestampTime","name":"Sessions","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"Body","serviceNameExpression":"ServiceName","bodyExpression":"Body","eventAttributesExpression":"LogAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,SeverityText,Body","severityTextExpression":"SeverityText","traceIdExpression":"TraceId","spanIdExpression":"SpanId","connection":"Local ClickHouse","logSourceId":"Logs","traceSourceId":"Traces","metricSourceId":"Metrics"}]' +export DEFAULT_SOURCES='[{"from":{"databaseName":"default","tableName":"otel_logs"},"kind":"log","timestampValueExpression":"Timestamp","name":"Logs","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"Body","serviceNameExpression":"ServiceName","bodyExpression":"Body","eventAttributesExpression":"LogAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,SeverityText,Body","severityTextExpression":"SeverityText","traceIdExpression":"TraceId","spanIdExpression":"SpanId","metadataMaterializedViews":{"kvRollupTable":"otel_logs_kv_rollup_15m","granularity":"15 minute"},"connection":"Local ClickHouse","traceSourceId":"Traces","metricSourceId":"Metrics"},{"from":{"databaseName":"default","tableName":"otel_traces"},"kind":"trace","timestampValueExpression":"Timestamp","name":"Traces","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"SpanName","serviceNameExpression":"ServiceName","eventAttributesExpression":"SpanAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,StatusCode,round(Duration/1e6),SpanName","traceIdExpression":"TraceId","spanIdExpression":"SpanId","durationExpression":"Duration","durationPrecision":9,"parentSpanIdExpression":"ParentSpanId","spanNameExpression":"SpanName","spanKindExpression":"SpanKind","statusCodeExpression":"StatusCode","statusMessageExpression":"StatusMessage","spanEventsValueExpression":"Events","spanLinksValueExpression":"Links","metadataMaterializedViews":{"kvRollupTable":"otel_traces_kv_rollup_15m","granularity":"15 minute"},"connection":"Local ClickHouse","logSourceId":"Logs","sessionSourceId":"Sessions","metricSourceId":"Metrics"},{"from":{"databaseName":"default","tableName":""},"kind":"metric","timestampValueExpression":"TimeUnix","name":"Metrics","resourceAttributesExpression":"ResourceAttributes","metricTables":{"gauge":"otel_metrics_gauge","histogram":"otel_metrics_histogram","sum":"otel_metrics_sum","summary":"otel_metrics_summary","exponential histogram":"otel_metrics_exponential_histogram"},"connection":"Local ClickHouse","logSourceId":"Logs"},{"from":{"databaseName":"default","tableName":"hyperdx_sessions"},"kind":"session","timestampValueExpression":"TimestampTime","name":"Sessions","resourceAttributesExpression":"ResourceAttributes","connection":"Local ClickHouse","traceSourceId":"Traces"}]' # if BETA_CH_OTEL_JSON_SCHEMA_ENABLED is true then return empty default sources if [ "$BETA_CH_OTEL_JSON_SCHEMA_ENABLED" = "true" ]; then - export DEFAULT_SOURCES='[{"from":{"databaseName":"default","tableName":"otel_logs"},"kind":"log","timestampValueExpression":"Timestamp","name":"Logs","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"Body","serviceNameExpression":"ServiceName","bodyExpression":"Body","eventAttributesExpression":"LogAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,SeverityText,Body","severityTextExpression":"SeverityText","traceIdExpression":"TraceId","spanIdExpression":"SpanId","connection":"Local ClickHouse","traceSourceId":"Traces","sessionSourceId":"Sessions","metricSourceId":"Metrics"},{"from":{"databaseName":"default","tableName":"otel_traces"},"kind":"trace","timestampValueExpression":"Timestamp","name":"Traces","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"SpanName","serviceNameExpression":"ServiceName","eventAttributesExpression":"SpanAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,StatusCode,round(Duration/1e6),SpanName","traceIdExpression":"TraceId","spanIdExpression":"SpanId","durationExpression":"Duration","durationPrecision":9,"parentSpanIdExpression":"ParentSpanId","spanNameExpression":"SpanName","spanKindExpression":"SpanKind","statusCodeExpression":"StatusCode","statusMessageExpression":"StatusMessage","connection":"Local ClickHouse","logSourceId":"Logs","sessionSourceId":"Sessions","metricSourceId":"Metrics"},{"from":{"databaseName":"default","tableName":""},"kind":"metric","timestampValueExpression":"TimeUnix","name":"Metrics","resourceAttributesExpression":"ResourceAttributes","metricTables":{"gauge":"otel_metrics_gauge","histogram":"otel_metrics_histogram","sum":"otel_metrics_sum","summary":"otel_metrics_summary","exponentialHistogram":"otel_metrics_exponential_histogram","_id":"682586a8b1f81924e628e808","id":"682586a8b1f81924e628e808"},"connection":"Local ClickHouse","logSourceId":"Logs","traceSourceId":"Traces","sessionSourceId":"Sessions"},{"from":{"databaseName":"default","tableName":"hyperdx_sessions"},"kind":"session","timestampValueExpression":"Timestamp","name":"Sessions","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"Body","serviceNameExpression":"ServiceName","bodyExpression":"Body","eventAttributesExpression":"LogAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,SeverityText,Body","severityTextExpression":"SeverityText","traceIdExpression":"TraceId","spanIdExpression":"SpanId","connection":"Local ClickHouse","logSourceId":"Logs","traceSourceId":"Traces","metricSourceId":"Metrics"}]' + export DEFAULT_SOURCES='[{"from":{"databaseName":"default","tableName":"otel_logs"},"kind":"log","timestampValueExpression":"Timestamp","name":"Logs","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"Body","serviceNameExpression":"ServiceName","bodyExpression":"Body","eventAttributesExpression":"LogAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,SeverityText,Body","severityTextExpression":"SeverityText","traceIdExpression":"TraceId","spanIdExpression":"SpanId","connection":"Local ClickHouse","traceSourceId":"Traces","metricSourceId":"Metrics"},{"from":{"databaseName":"default","tableName":"otel_traces"},"kind":"trace","timestampValueExpression":"Timestamp","name":"Traces","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"SpanName","serviceNameExpression":"ServiceName","eventAttributesExpression":"SpanAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,StatusCode,round(Duration/1e6),SpanName","traceIdExpression":"TraceId","spanIdExpression":"SpanId","durationExpression":"Duration","durationPrecision":9,"parentSpanIdExpression":"ParentSpanId","spanNameExpression":"SpanName","spanKindExpression":"SpanKind","statusCodeExpression":"StatusCode","statusMessageExpression":"StatusMessage","spanEventsValueExpression":"Events","spanLinksValueExpression":"Links","connection":"Local ClickHouse","logSourceId":"Logs","sessionSourceId":"Sessions","metricSourceId":"Metrics"},{"from":{"databaseName":"default","tableName":""},"kind":"metric","timestampValueExpression":"TimeUnix","name":"Metrics","resourceAttributesExpression":"ResourceAttributes","metricTables":{"gauge":"otel_metrics_gauge","histogram":"otel_metrics_histogram","sum":"otel_metrics_sum","summary":"otel_metrics_summary","exponential histogram":"otel_metrics_exponential_histogram"},"connection":"Local ClickHouse","logSourceId":"Logs"},{"from":{"databaseName":"default","tableName":"hyperdx_sessions"},"kind":"session","timestampValueExpression":"Timestamp","name":"Sessions","resourceAttributesExpression":"ResourceAttributes","connection":"Local ClickHouse","traceSourceId":"Traces"}]' fi # Simulate Docker Service DNS diff --git a/packages/api/.env.development b/packages/api/.env.development index d4995195fd..1b71f80e83 100644 --- a/packages/api/.env.development +++ b/packages/api/.env.development @@ -22,7 +22,7 @@ USAGE_STATS_ENABLED=false NODE_OPTIONS="--max-http-header-size=131072" ENABLE_SWAGGER=true DEFAULT_CONNECTIONS=[{"name":"Local ClickHouse","host":"http://localhost:${HDX_DEV_CH_HTTP_PORT}","username":"default","password":""}] -DEFAULT_SOURCES=[{"from":{"databaseName":"default","tableName":"otel_logs"},"kind":"log","timestampValueExpression":"Timestamp","name":"Logs","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"Body","serviceNameExpression":"ServiceName","bodyExpression":"Body","eventAttributesExpression":"LogAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,SeverityText,Body","severityTextExpression":"SeverityText","traceIdExpression":"TraceId","spanIdExpression":"SpanId","metadataMaterializedViews":{"keyRollupTable":"otel_logs_key_rollup_15m","kvRollupTable":"otel_logs_kv_rollup_15m","granularity":"15 minute"},"connection":"Local ClickHouse","traceSourceId":"Traces","sessionSourceId":"Sessions","metricSourceId":"Metrics"},{"from":{"databaseName":"default","tableName":"otel_traces"},"kind":"trace","timestampValueExpression":"Timestamp","name":"Traces","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"SpanName","serviceNameExpression":"ServiceName","eventAttributesExpression":"SpanAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,StatusCode,round(Duration/1e6),SpanName","traceIdExpression":"TraceId","spanIdExpression":"SpanId","durationExpression":"Duration","durationPrecision":9,"parentSpanIdExpression":"ParentSpanId","spanNameExpression":"SpanName","spanKindExpression":"SpanKind","statusCodeExpression":"StatusCode","statusMessageExpression":"StatusMessage","metadataMaterializedViews":{"keyRollupTable":"otel_traces_key_rollup_15m","kvRollupTable":"otel_traces_kv_rollup_15m","granularity":"15 minute"},"connection":"Local ClickHouse","logSourceId":"Logs","sessionSourceId":"Sessions","metricSourceId":"Metrics"},{"from":{"databaseName":"default","tableName":""},"kind":"metric","timestampValueExpression":"TimeUnix","name":"Metrics","resourceAttributesExpression":"ResourceAttributes","metricTables":{"gauge":"otel_metrics_gauge","histogram":"otel_metrics_histogram","sum":"otel_metrics_sum","_id":"682586a8b1f81924e628e808","id":"682586a8b1f81924e628e808"},"connection":"Local ClickHouse","logSourceId":"Logs","traceSourceId":"Traces","sessionSourceId":"Sessions"},{"from":{"databaseName":"default","tableName":"hyperdx_sessions"},"kind":"session","timestampValueExpression":"TimestampTime","name":"Sessions","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"Body","serviceNameExpression":"ServiceName","bodyExpression":"Body","eventAttributesExpression":"LogAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,SeverityText,Body","severityTextExpression":"SeverityText","traceIdExpression":"TraceId","spanIdExpression":"SpanId","connection":"Local ClickHouse","logSourceId":"Logs","traceSourceId":"Traces","metricSourceId":"Metrics"},{"from":{"databaseName":"otel_json","tableName":"otel_logs"},"kind":"log","timestampValueExpression":"Timestamp","name":"JSON Logs","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"Body","serviceNameExpression":"ServiceName","bodyExpression":"Body","eventAttributesExpression":"LogAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,SeverityText,Body","severityTextExpression":"SeverityText","traceIdExpression":"TraceId","spanIdExpression":"SpanId","connection":"Local ClickHouse","traceSourceId":"JSON Traces","metricSourceId":"JSON Metrics"},{"from":{"databaseName":"otel_json","tableName":"otel_traces"},"kind":"trace","timestampValueExpression":"Timestamp","name":"JSON Traces","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"SpanName","serviceNameExpression":"ServiceName","eventAttributesExpression":"SpanAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,StatusCode,round(Duration/1e6),SpanName","traceIdExpression":"TraceId","spanIdExpression":"SpanId","durationExpression":"Duration","durationPrecision":9,"parentSpanIdExpression":"ParentSpanId","spanNameExpression":"SpanName","spanKindExpression":"SpanKind","statusCodeExpression":"StatusCode","statusMessageExpression":"StatusMessage","connection":"Local ClickHouse","logSourceId":"JSON Logs","metricSourceId":"JSON Metrics"},{"from":{"databaseName":"otel_json","tableName":""},"kind":"metric","timestampValueExpression":"TimeUnix","name":"JSON Metrics","resourceAttributesExpression":"ResourceAttributes","metricTables":{"gauge":"otel_metrics_gauge","histogram":"otel_metrics_histogram","sum":"otel_metrics_sum"},"connection":"Local ClickHouse","logSourceId":"JSON Logs","traceSourceId":"JSON Traces"},{"kind":"promql","name":"PromQL","connection":"Local ClickHouse","from":{"databaseName":"default","tableName":"metrics_ts"},"timestampValueExpression":"timestamp"}] +DEFAULT_SOURCES=[{"from":{"databaseName":"default","tableName":"otel_logs"},"kind":"log","timestampValueExpression":"Timestamp","name":"Logs","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"Body","serviceNameExpression":"ServiceName","bodyExpression":"Body","eventAttributesExpression":"LogAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,SeverityText,Body","severityTextExpression":"SeverityText","traceIdExpression":"TraceId","spanIdExpression":"SpanId","metadataMaterializedViews":{"kvRollupTable":"otel_logs_kv_rollup_15m","granularity":"15 minute"},"connection":"Local ClickHouse","traceSourceId":"Traces","metricSourceId":"Metrics"},{"from":{"databaseName":"default","tableName":"otel_traces"},"kind":"trace","timestampValueExpression":"Timestamp","name":"Traces","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"SpanName","serviceNameExpression":"ServiceName","eventAttributesExpression":"SpanAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,StatusCode,round(Duration/1e6),SpanName","traceIdExpression":"TraceId","spanIdExpression":"SpanId","durationExpression":"Duration","durationPrecision":9,"parentSpanIdExpression":"ParentSpanId","spanNameExpression":"SpanName","spanKindExpression":"SpanKind","statusCodeExpression":"StatusCode","statusMessageExpression":"StatusMessage","spanEventsValueExpression":"Events","spanLinksValueExpression":"Links","metadataMaterializedViews":{"kvRollupTable":"otel_traces_kv_rollup_15m","granularity":"15 minute"},"connection":"Local ClickHouse","logSourceId":"Logs","sessionSourceId":"Sessions","metricSourceId":"Metrics"},{"from":{"databaseName":"default","tableName":""},"kind":"metric","timestampValueExpression":"TimeUnix","name":"Metrics","resourceAttributesExpression":"ResourceAttributes","metricTables":{"gauge":"otel_metrics_gauge","histogram":"otel_metrics_histogram","sum":"otel_metrics_sum","summary":"otel_metrics_summary","exponential histogram":"otel_metrics_exponential_histogram"},"connection":"Local ClickHouse","logSourceId":"Logs"},{"from":{"databaseName":"default","tableName":"hyperdx_sessions"},"kind":"session","timestampValueExpression":"TimestampTime","name":"Sessions","resourceAttributesExpression":"ResourceAttributes","connection":"Local ClickHouse","traceSourceId":"Traces"},{"from":{"databaseName":"otel_json","tableName":"otel_logs"},"kind":"log","timestampValueExpression":"Timestamp","name":"JSON Logs","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"Body","serviceNameExpression":"ServiceName","bodyExpression":"Body","eventAttributesExpression":"LogAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,SeverityText,Body","severityTextExpression":"SeverityText","traceIdExpression":"TraceId","spanIdExpression":"SpanId","connection":"Local ClickHouse","traceSourceId":"JSON Traces","metricSourceId":"JSON Metrics"},{"from":{"databaseName":"otel_json","tableName":"otel_traces"},"kind":"trace","timestampValueExpression":"Timestamp","name":"JSON Traces","displayedTimestampValueExpression":"Timestamp","implicitColumnExpression":"SpanName","serviceNameExpression":"ServiceName","eventAttributesExpression":"SpanAttributes","resourceAttributesExpression":"ResourceAttributes","defaultTableSelectExpression":"Timestamp,ServiceName,StatusCode,round(Duration/1e6),SpanName","traceIdExpression":"TraceId","spanIdExpression":"SpanId","durationExpression":"Duration","durationPrecision":9,"parentSpanIdExpression":"ParentSpanId","spanNameExpression":"SpanName","spanKindExpression":"SpanKind","statusCodeExpression":"StatusCode","statusMessageExpression":"StatusMessage","spanEventsValueExpression":"Events","spanLinksValueExpression":"Links","connection":"Local ClickHouse","logSourceId":"JSON Logs","metricSourceId":"JSON Metrics"},{"from":{"databaseName":"otel_json","tableName":""},"kind":"metric","timestampValueExpression":"TimeUnix","name":"JSON Metrics","resourceAttributesExpression":"ResourceAttributes","metricTables":{"gauge":"otel_metrics_gauge","histogram":"otel_metrics_histogram","sum":"otel_metrics_sum","summary":"otel_metrics_summary","exponential histogram":"otel_metrics_exponential_histogram"},"connection":"Local ClickHouse","logSourceId":"JSON Logs"},{"kind":"promql","name":"PromQL","connection":"Local ClickHouse","from":{"databaseName":"default","tableName":"metrics_ts"},"timestampValueExpression":"timestamp"}] INGESTION_API_KEY="super-secure-ingestion-api-key" HYPERDX_API_KEY=$INGESTION_API_KEY diff --git a/packages/app/src/components/DBRowDataPanel.tsx b/packages/app/src/components/DBRowDataPanel.tsx index 8540e06c5a..ddf77f48f5 100644 --- a/packages/app/src/components/DBRowDataPanel.tsx +++ b/packages/app/src/components/DBRowDataPanel.tsx @@ -261,7 +261,7 @@ export function useRowData({ const isLoading = queryResult.isLoading || (isBoundedEmpty && queryResult.isPending); - // Normalize resource and event attributes to always use flat keys for both JSON and Map columns + // Normalize resource, event and span-event exception attributes to always use flat keys for both JSON and Map columns const normalizedData = useMemo(() => { if (!queryResult.data?.data?.[0]) { return queryResult.data; @@ -282,6 +282,12 @@ export function useRowData({ ); } + if (row[ROW_DATA_ALIASES.EVENTS_EXCEPTION_ATTRIBUTES]) { + normalizedRow[ROW_DATA_ALIASES.EVENTS_EXCEPTION_ATTRIBUTES] = flatten( + row[ROW_DATA_ALIASES.EVENTS_EXCEPTION_ATTRIBUTES], + ); + } + return { ...queryResult.data, data: [normalizedRow], diff --git a/packages/app/src/components/DBRowOverviewPanel.tsx b/packages/app/src/components/DBRowOverviewPanel.tsx index c9acc5c42b..6f7602a2f3 100644 --- a/packages/app/src/components/DBRowOverviewPanel.tsx +++ b/packages/app/src/components/DBRowOverviewPanel.tsx @@ -9,6 +9,7 @@ import { getEventBody } from '@/source'; import { getHighlightedAttributesFromData } from '@/utils/highlightedAttributes'; import { resolveRowTimestampAnchor } from '@/utils/rowTimestamps'; import { getRequestPanelTitle } from '@/utils/rpcSpanAttributes'; +import { isSet } from '@/utils/spanAttributeReaders'; import { getJSONColumnNames, @@ -27,6 +28,12 @@ import { SpanLinkedFromSubpanel } from './SpanLinkedFromSubpanel'; import { getValidSpanLinks, SpanLinksSubpanel } from './SpanLinksSubpanel'; const EMPTY_OBJ = {}; +const EXCEPTION_ATTRIBUTE_KEYS = [ + 'exception.type', + 'exception.message', + 'exception.stacktrace', +]; + export function RowOverviewPanel({ source, rowId, @@ -140,16 +147,30 @@ export function RowOverviewPanel({ [flattenedEventAttributes], ); + const exceptionAttributes = useMemo(() => { + const spanEventAttributes = firstRow?.__hdx_events_exception_attributes; + if ( + spanEventAttributes != null && + Object.keys(spanEventAttributes).length > 0 + ) { + return spanEventAttributes; + } + return EXCEPTION_ATTRIBUTE_KEYS.some(key => + isSet(flattenedEventAttributes[key]), + ) + ? flattenedEventAttributes + : undefined; + }, [firstRow?.__hdx_events_exception_attributes, flattenedEventAttributes]); + const exceptionValues = useMemo(() => { - const parsedEvents = - firstRow?.__hdx_events_exception_attributes ?? EMPTY_OBJ; + const parsedEvents = exceptionAttributes ?? EMPTY_OBJ; const stacktrace = parsedEvents?.['exception.stacktrace'] || parsedEvents?.['exception.parsed_stacktrace']; let parsedStacktrace = stacktrace ?? '[]'; try { - parsedStacktrace = JSON.parse(stacktrace); + parsedStacktrace = JSON.parse(parsedStacktrace); } catch { // do nothing } @@ -165,13 +186,9 @@ export function RowOverviewPanel({ mechanism: parsedEvents?.['exception.mechanism'], }, ]; - }, [firstRow]); + }, [exceptionAttributes]); - const hasException = useMemo(() => { - return ( - Object.keys(firstRow?.__hdx_events_exception_attributes ?? {}).length > 0 - ); - }, [firstRow?.__hdx_events_exception_attributes]); + const hasException = exceptionAttributes != null; const hasSpanEvents = useMemo(() => { return ( @@ -252,39 +269,38 @@ export function RowOverviewPanel({ multiple variant="noPadding" > - {requestPanelTitle != null && ( - + {hasException && ( + - {requestPanelTitle} + Exception - )} - {hasException && ( - + {requestPanelTitle != null && ( + - Exception + {requestPanelTitle} - diff --git a/packages/app/src/components/DBTraceWaterfallChart.tsx b/packages/app/src/components/DBTraceWaterfallChart.tsx index bc8c4c3748..759fdebe29 100644 --- a/packages/app/src/components/DBTraceWaterfallChart.tsx +++ b/packages/app/src/components/DBTraceWaterfallChart.tsx @@ -115,8 +115,8 @@ type TimestampedRow = { Timestamp: string; }; -// Bar background color. Correlated log rows are always green (success), error -// spans are always red, and every other span takes its per-service color. +// Bar background color. Error spans and logs are red, other correlated log rows +// are green (success), and every other span takes its per-service color. function barColor(condition: { isHighlighted: boolean; isError?: boolean; @@ -125,7 +125,7 @@ function barColor(condition: { }) { const { isHighlighted, isError, type, serviceColor } = condition; - if (type === SourceKind.Log) { + if (type === SourceKind.Log && !isError) { return isHighlighted ? getChartColorSuccessHighlight() : getChartColorSuccess(); @@ -161,7 +161,7 @@ function getRowSeverity( } // Per-service span palette: the full categorical palette minus the two hues we -// reserve for semantics — green (correlated log rows) and red (error spans) — +// reserve for semantics — green (correlated log rows) and red (errors) — // so a service color is never confused with a log or an error. Index-aligned // with CATEGORICAL_PALETTE_TOKENS, so filtering by token survives reordering. const SERVICE_COLORS = COLORS.filter( @@ -1040,8 +1040,9 @@ export function DBTraceWaterfallChartContainer({ for (const result of rows ?? []) { const { type, SpanId, ParentSpanId } = result; - // ignore everything without spanId - if (!SpanId) continue; + // A span without an id can't be placed; a log without one still belongs + // to the trace. + if (!SpanId && type !== SourceKind.Log) continue; // log have duplicate span id, tag it with -log const nodeSpanId = type === SourceKind.Log ? `${SpanId}-log` : SpanId; // prevent log spanId overwrite trace spanId @@ -1073,10 +1074,10 @@ export function DBTraceWaterfallChartContainer({ nodesMap.set(result.id, curNode); } - // root if: is trace event, and (has no parent or parent id is not valid) + // Root when the parent isn't a span in this trace, including a log whose + // span was never exported (e.g. the process crashed before ending it). const isRootNode = - type === SourceKind.Trace && - (!nodeParentSpanId || !validSpanIDs.has(nodeParentSpanId)); + !nodeParentSpanId || !validSpanIDs.has(nodeParentSpanId); if (isRootNode) { rootNodes.push(curNode); @@ -1243,11 +1244,15 @@ export function DBTraceWaterfallChartContainer({ // Process span events into markers (only if showSpanEvents is enabled) const markers = showSpanEvents && result.SpanEvents - ? result.SpanEvents.map(spanEvent => ({ - timestamp: parseTimestampToMs(spanEvent.Timestamp) - minOffset, - name: spanEvent.Name, - attributes: spanEvent.Attributes || {}, - })) + ? result.SpanEvents.map(spanEvent => { + const epochMs = parseTimestampToMs(spanEvent.Timestamp); + return { + timestamp: epochMs - minOffset, + epochMs, + name: spanEvent.Name, + attributes: spanEvent.Attributes || {}, + }; + }) : []; // Extract status logic @@ -1261,7 +1266,9 @@ export function DBTraceWaterfallChartContainer({ const barBackgroundColor = type === SourceKind.Log - ? getChartColorSuccess() + ? isError + ? getChartColorError() + : getChartColorSuccess() : serviceName ? (serviceColorMap.get(serviceName) ?? '#6A7077') : '#6A7077'; diff --git a/packages/app/src/components/ExceptionSubpanel.tsx b/packages/app/src/components/ExceptionSubpanel.tsx index d763ae807b..39f69e0d2a 100644 --- a/packages/app/src/components/ExceptionSubpanel.tsx +++ b/packages/app/src/components/ExceptionSubpanel.tsx @@ -506,7 +506,9 @@ export const ExceptionSubpanel = ({ { category: 'exception', timestamp: new Date(logData?.timestamp ?? 0).getTime() / 1000, - message: `${firstException.type}: ${firstException.value} `, + message: [firstException.type, firstException.value] + .filter(Boolean) + .join(': '), }, ] : []), @@ -541,10 +543,15 @@ export const ExceptionSubpanel = ({ firstException && ( <>
- - {firstException.type}:{' '} - - {firstException.value} + {firstException.type && ( + + {firstException.type} + + )} + + {firstException.type && firstException.value && ': '} + {firstException.value} +
{firstException.mechanism && ( diff --git a/packages/app/src/components/TimelineChart/TimelineChart.stories.tsx b/packages/app/src/components/TimelineChart/TimelineChart.stories.tsx index f93e1ae115..36049d9f2f 100644 --- a/packages/app/src/components/TimelineChart/TimelineChart.stories.tsx +++ b/packages/app/src/components/TimelineChart/TimelineChart.stories.tsx @@ -226,6 +226,7 @@ const markedRows = (() => { const makeMarkers = (base: number): TTimelineSpanEventMarker[] => [0, 20, 45, 80].map((offset, idx) => ({ timestamp: base + offset, + epochMs: Date.UTC(2024, 0, 1) + base + offset, name: `span.event.${idx}`, attributes: { 'event.index': idx, diff --git a/packages/app/src/components/TimelineChart/TimelineSpanEventMarker.tsx b/packages/app/src/components/TimelineChart/TimelineSpanEventMarker.tsx index 3c7f87ec70..ad142ad5be 100644 --- a/packages/app/src/components/TimelineChart/TimelineSpanEventMarker.tsx +++ b/packages/app/src/components/TimelineChart/TimelineSpanEventMarker.tsx @@ -5,6 +5,7 @@ import { useFormatTime } from '@/useFormatTime'; export type TTimelineSpanEventMarker = { timestamp: number; // ms offset from minOffset + epochMs: number; // absolute time, shown in the tooltip name: string; attributes: Record; }; @@ -21,6 +22,11 @@ export const TimelineSpanEventMarker = memo(function ({ height: number; }) { const formatTime = useFormatTime(); + // OTel records exceptions on spans as events named `exception`. + const isException = marker.name === 'exception'; + const markerColor = isException + ? 'var(--color-bg-danger)' + : 'var(--color-bg-success)'; const spanDuration = eventEnd - eventStart; const markerOffsetFromStart = marker.timestamp - eventStart; const markerPosition = @@ -30,14 +36,22 @@ export const TimelineSpanEventMarker = memo(function ({ const tooltipContent = (
- {formatTime(new Date(marker.timestamp), { format: 'withMs' })} + {formatTime(new Date(marker.epochMs), { format: 'withMs' })} {marker.name} {attributeEntries.length > 0 && (
{attributeEntries.slice(0, 5).map(([key, value]) => (
- {key}:{' '} + + {key}: + {' '} {String(value).length > 50 ? String(value).substring(0, 50) + '...' : String(value)} @@ -85,7 +99,7 @@ export const TimelineSpanEventMarker = memo(function ({ style={{ width: 8, height: 8, - backgroundColor: 'var(--color-bg-success)', + backgroundColor: markerColor, transform: 'rotate(45deg)', border: '1px solid #333', boxShadow: '0 1px 3px rgba(0,0,0,0.3)', @@ -99,7 +113,7 @@ export const TimelineSpanEventMarker = memo(function ({ transform: 'translate(-50%, -50%)', width: 1, height: height, - backgroundColor: 'var(--color-bg-success)', + backgroundColor: markerColor, opacity: 0.4, zIndex: -1, }} diff --git a/packages/app/src/components/TimelineChart/__tests__/TimelineChartRowEvents.test.tsx b/packages/app/src/components/TimelineChart/__tests__/TimelineChartRowEvents.test.tsx index bcf17b2f66..96d9f6a5bc 100644 --- a/packages/app/src/components/TimelineChart/__tests__/TimelineChartRowEvents.test.tsx +++ b/packages/app/src/components/TimelineChart/__tests__/TimelineChartRowEvents.test.tsx @@ -1,3 +1,6 @@ +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + import { TimelineChartRowEvents, type TTimelineEvent, @@ -57,3 +60,58 @@ describe('TimelineChartRowEvents width', () => { ); }); }); + +describe('TimelineChartRowEvents span event markers', () => { + it('colors exception events red and other events green', () => { + const { container } = renderRow( + [ + makeEvent({ + end: 10, + markers: [ + { timestamp: 2, epochMs: 2, name: 'exception', attributes: {} }, + { timestamp: 6, epochMs: 6, name: 'cache.miss', attributes: {} }, + ], + }), + ], + 10, + ); + + const diamonds = Array.from( + container.querySelectorAll('[style*="rotate(45deg)"]'), + ); + expect(diamonds.map(d => d.style.backgroundColor)).toEqual([ + 'var(--color-bg-danger)', + 'var(--color-bg-success)', + ]); + }); + + it('shows the event time and red attribute keys in an exception tooltip', async () => { + const { container } = renderRow( + [ + makeEvent({ + end: 10, + markers: [ + { + timestamp: 2, + epochMs: Date.UTC(2024, 5, 15, 12, 0, 0, 345), + name: 'exception', + attributes: { 'exception.type': 'TypeError' }, + }, + ], + }), + ], + 10, + ); + + await userEvent.hover( + container.querySelector('[style*="rotate(45deg)"]')! + .parentElement!, + ); + + expect((await screen.findByText('exception.type:')).style.color).toBe( + 'var(--color-text-danger)', + ); + // The absolute event time, not its offset from the trace start. + expect(screen.getByText(/^Jun 1[56] .*\.345/)).toBeInTheDocument(); + }); +}); diff --git a/packages/app/src/components/__tests__/DBRowDataPanel.test.ts b/packages/app/src/components/__tests__/DBRowDataPanel.test.ts index 94a5de0e2e..96c7326439 100644 --- a/packages/app/src/components/__tests__/DBRowDataPanel.test.ts +++ b/packages/app/src/components/__tests__/DBRowDataPanel.test.ts @@ -80,6 +80,35 @@ describe('DBRowDataPanel', () => { expect(config.select[0]).toEqual({ valueExpression: '*' }); }); + // JSON-typed Events.Attributes come back nested. + it('flattens span-event exception attributes', () => { + mockUseQueriedChartConfig.mockReturnValue({ + data: { + data: [ + { + __hdx_events_exception_attributes: { + exception: { type: 'TypeError' }, + }, + }, + ], + meta: [], + rows: 1, + isComplete: true, + }, + isLoading: false, + isError: false, + isSuccess: true, + }); + + const { result } = renderHook(() => + useRowData({ source, rowId: "id='abc123'" }), + ); + + expect( + result.current.data?.data[0].__hdx_events_exception_attributes, + ).toEqual({ 'exception.type': 'TypeError' }); + }); + it('selects the Known Columns List instead of `*` when set', () => { const sourceWithKnownColumns: TLogSource = { ...source, diff --git a/packages/app/src/components/__tests__/DBRowOverviewPanel.exception.test.tsx b/packages/app/src/components/__tests__/DBRowOverviewPanel.exception.test.tsx new file mode 100644 index 0000000000..adcb5ecc2a --- /dev/null +++ b/packages/app/src/components/__tests__/DBRowOverviewPanel.exception.test.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types'; + +import { RowOverviewPanel } from '@/components/DBRowOverviewPanel'; + +let mockRow: Record; +jest.mock('../DBRowDataPanel', () => ({ + useRowData: () => ({ data: { data: [mockRow] } }), + getJSONColumnNames: () => [], + getMapColumnNames: () => [], +})); + +const mockExceptionSubpanel = jest.fn(); +jest.mock('../ExceptionSubpanel', () => ({ + ExceptionSubpanel: (props: unknown) => mockExceptionSubpanel(props), +})); + +jest.mock('../DBRowJsonViewer', () => ({ DBRowJsonViewer: () => null })); + +jest.mock('../linkedSpans', () => ({ + useReverseSpanLinks: () => ({ links: [] }), + useLinkedSpanDetails: () => ({ details: new Map() }), +})); + +// `any`, not `as`, keeps this partial fixture off the lint warning budget. +const asSource = (value: any): TSource => value; +const LOG_SOURCE = asSource({ + kind: SourceKind.Log, + eventAttributesExpression: 'LogAttributes', +}); + +function renderRow(row: Record) { + mockRow = row; + renderWithMantine(); +} + +const exceptionShown = () => + mockExceptionSubpanel.mock.calls[0]?.[0].exceptionValues[0]; + +describe('RowOverviewPanel exception section', () => { + beforeEach(() => jest.clearAllMocks()); + + it('reads exception.* from the row attributes', () => { + renderRow({ + __hdx_event_attributes: { + 'exception.type': 'TypeError', + 'exception.message': 'x is undefined', + 'exception.stacktrace': 'TypeError: x is undefined\n at main.js:1', + }, + }); + + expect(exceptionShown()).toMatchObject({ + type: 'TypeError', + value: 'x is undefined', + stacktrace: 'TypeError: x is undefined\n at main.js:1', + }); + }); + + it('passes no stacktrace text when the row has none', () => { + renderRow({ + __hdx_event_attributes: { + 'exception.type': 'ValueError', + 'exception.message': 'bad input', + }, + }); + + expect(exceptionShown().stacktrace).toEqual([]); + }); + + it('prefers the exception span event over the row attributes', () => { + renderRow({ + __hdx_event_attributes: { 'exception.type': 'FromAttributes' }, + __hdx_events_exception_attributes: { 'exception.type': 'FromEvent' }, + }); + + expect(exceptionShown()).toMatchObject({ type: 'FromEvent' }); + }); + + it('hides the section without exception attributes', () => { + renderRow({ + __hdx_event_attributes: { 'http.request.method': 'GET' }, + __hdx_events_exception_attributes: {}, + }); + + expect(mockExceptionSubpanel).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/app/src/components/__tests__/DBTraceWaterfallChart.test.tsx b/packages/app/src/components/__tests__/DBTraceWaterfallChart.test.tsx index 832764f37d..eb498c616d 100644 --- a/packages/app/src/components/__tests__/DBTraceWaterfallChart.test.tsx +++ b/packages/app/src/components/__tests__/DBTraceWaterfallChart.test.tsx @@ -25,6 +25,7 @@ import { TRACE_WATERFALL_ROW_LIMIT } from '@/components/traceWaterfallLimits'; import { useColumns } from '@/hooks/useMetadata'; import useOffsetPaginatedQuery from '@/hooks/useOffsetPaginatedQuery'; import useRowWhere from '@/hooks/useRowWhere'; +import { getChartColorError, getChartColorSuccess } from '@/utils'; // Mock setup jest.mock('@/components/traceWaterfallLimits', () => ({ @@ -338,6 +339,42 @@ describe('DBTraceWaterfallChartContainer', () => { expect(MockTimelineChart.latestProps.rows[1]).toBeTruthy(); // Log row }); + it.each([ + ['whose span was never exported', 'unsent-span'], + ['without a span id', ''], + ])('renders a log %s at the root', async (_, SpanId) => { + setupQueryMocks({ + traceData: mockTraceData, + logData: { ...mockLogData, data: [{ ...mockLogData.data[0], SpanId }] }, + }); + + renderComponent(); + await waitForLoading(); + + expect(MockTimelineChart.latestProps.rows.length).toBe(2); + }); + + it.each([ + ['error', () => getChartColorError()], + ['warn', () => getChartColorSuccess()], + ])('colors a %s log bar', async (SeverityText, expectedColor) => { + setupQueryMocks({ + traceData: mockTraceData, + logData: { + ...mockLogData, + data: [{ ...mockLogData.data[0], SeverityText }], + }, + }); + + renderComponent(); + await waitForLoading(); + + const logRow = MockTimelineChart.latestProps.rows.find( + (row: { type?: string }) => row.type === SourceKind.Log, + ); + expect(logRow.events[0].backgroundColor).toBe(expectedColor()); + }); + it('renders total duration computed from all fetched spans', async () => { setupQueryMocks({ traceData: mockTraceData }); renderComponent(null); // No log table source @@ -468,17 +505,11 @@ describe('DBTraceWaterfallChartContainer', () => { }); it('does not render the total duration stat when there are no spans', async () => { - // A log with no matching span never becomes a visible node (correlated - // logs attach to a span), so this renders the existing empty state -- - // just confirms the new stat doesn't render in that case either. setupQueryMocks({ logData: mockLogData }); renderComponent(); + await waitForLoading(); - await waitFor(() => { - expect( - screen.getByText('No matching spans or logs found'), - ).toBeInTheDocument(); - }); + expect(MockTimelineChart.latestProps.rows.length).toBe(1); expect(screen.queryByTestId('trace-total-stats')).not.toBeInTheDocument(); }); diff --git a/packages/app/src/components/__tests__/ExceptionSubpanel.test.tsx b/packages/app/src/components/__tests__/ExceptionSubpanel.test.tsx new file mode 100644 index 0000000000..7a8c90b7cd --- /dev/null +++ b/packages/app/src/components/__tests__/ExceptionSubpanel.test.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; + +import { ExceptionSubpanel } from '@/components/ExceptionSubpanel'; + +describe('ExceptionSubpanel', () => { + it.each([ + [{ type: '', value: 'bad input' }, 'bad input'], + [{ type: 'ValueError', value: '' }, 'ValueError'], + ])('labels %o as "%s" without a dangling colon', (exception, label) => { + renderWithMantine( + , + ); + + expect(screen.getByText(label).closest('div')?.textContent).toBe(label); + expect(screen.queryByText('Breadcrumbs')).not.toBeInTheDocument(); + }); +});