mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-26 03:40:33 +01:00
Compare commits
6 Commits
fix/ext-ap
...
feat/add-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0aec7a9fe8 | ||
|
|
bebe4ebb89 | ||
|
|
2209708caa | ||
|
|
1f813ce21f | ||
|
|
f082821ac2 | ||
|
|
3c5bd81421 |
@@ -60,14 +60,6 @@ web:
|
||||
index: index.html
|
||||
# The directory containing the static build files.
|
||||
directory: /etc/signoz/web
|
||||
# Settings exposed to the web.
|
||||
settings:
|
||||
posthog:
|
||||
# Whether to enable PostHog in web.
|
||||
enabled: true
|
||||
appcues:
|
||||
# Whether to enable Appcues in web.
|
||||
enabled: true
|
||||
|
||||
##################### Cache #####################
|
||||
cache:
|
||||
|
||||
@@ -5641,19 +5641,6 @@ components:
|
||||
type: object
|
||||
Sigv4SigV4Config:
|
||||
type: object
|
||||
SpantypesEvent:
|
||||
properties:
|
||||
attributeMap:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
isError:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
timeUnixNano:
|
||||
minimum: 0
|
||||
type: integer
|
||||
type: object
|
||||
SpantypesFieldContext:
|
||||
enum:
|
||||
- attribute
|
||||
@@ -5668,50 +5655,6 @@ components:
|
||||
required:
|
||||
- items
|
||||
type: object
|
||||
SpantypesGettableWaterfallTrace:
|
||||
properties:
|
||||
aggregations:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesSpanAggregationResult'
|
||||
nullable: true
|
||||
type: array
|
||||
endTimestampMillis:
|
||||
minimum: 0
|
||||
type: integer
|
||||
hasMissingSpans:
|
||||
type: boolean
|
||||
hasMore:
|
||||
type: boolean
|
||||
rootServiceEntryPoint:
|
||||
type: string
|
||||
rootServiceName:
|
||||
type: string
|
||||
serviceNameToTotalDurationMap:
|
||||
additionalProperties:
|
||||
minimum: 0
|
||||
type: integer
|
||||
nullable: true
|
||||
type: object
|
||||
spans:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesWaterfallSpan'
|
||||
nullable: true
|
||||
type: array
|
||||
startTimestampMillis:
|
||||
minimum: 0
|
||||
type: integer
|
||||
totalErrorSpansCount:
|
||||
minimum: 0
|
||||
type: integer
|
||||
totalSpansCount:
|
||||
minimum: 0
|
||||
type: integer
|
||||
uncollapsedSpans:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
SpantypesPostableSpanMapper:
|
||||
properties:
|
||||
config:
|
||||
@@ -5739,50 +5682,6 @@ components:
|
||||
- name
|
||||
- condition
|
||||
type: object
|
||||
SpantypesPostableWaterfall:
|
||||
properties:
|
||||
aggregations:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesSpanAggregation'
|
||||
nullable: true
|
||||
type: array
|
||||
limit:
|
||||
minimum: 0
|
||||
type: integer
|
||||
selectedSpanId:
|
||||
type: string
|
||||
uncollapsedSpans:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
SpantypesSpanAggregation:
|
||||
properties:
|
||||
aggregation:
|
||||
$ref: '#/components/schemas/SpantypesSpanAggregationType'
|
||||
field:
|
||||
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
|
||||
type: object
|
||||
SpantypesSpanAggregationResult:
|
||||
properties:
|
||||
aggregation:
|
||||
$ref: '#/components/schemas/SpantypesSpanAggregationType'
|
||||
field:
|
||||
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
|
||||
value:
|
||||
additionalProperties:
|
||||
minimum: 0
|
||||
type: integer
|
||||
nullable: true
|
||||
type: object
|
||||
type: object
|
||||
SpantypesSpanAggregationType:
|
||||
enum:
|
||||
- span_count
|
||||
- execution_time_percentage
|
||||
- duration
|
||||
type: string
|
||||
SpantypesSpanMapper:
|
||||
properties:
|
||||
config:
|
||||
@@ -5913,78 +5812,6 @@ components:
|
||||
nullable: true
|
||||
type: string
|
||||
type: object
|
||||
SpantypesWaterfallSpan:
|
||||
properties:
|
||||
attributes:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
type: object
|
||||
db_name:
|
||||
type: string
|
||||
db_operation:
|
||||
type: string
|
||||
duration_nano:
|
||||
minimum: 0
|
||||
type: integer
|
||||
events:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesEvent'
|
||||
nullable: true
|
||||
type: array
|
||||
external_http_method:
|
||||
type: string
|
||||
external_http_url:
|
||||
type: string
|
||||
flags:
|
||||
minimum: 0
|
||||
type: integer
|
||||
has_children:
|
||||
type: boolean
|
||||
has_error:
|
||||
type: boolean
|
||||
http_host:
|
||||
type: string
|
||||
http_method:
|
||||
type: string
|
||||
http_url:
|
||||
type: string
|
||||
is_remote:
|
||||
type: string
|
||||
kind_string:
|
||||
type: string
|
||||
level:
|
||||
minimum: 0
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
parent_span_id:
|
||||
type: string
|
||||
resource:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
response_status_code:
|
||||
type: string
|
||||
span_id:
|
||||
type: string
|
||||
status_code:
|
||||
type: integer
|
||||
status_code_string:
|
||||
type: string
|
||||
status_message:
|
||||
type: string
|
||||
sub_tree_node_count:
|
||||
minimum: 0
|
||||
type: integer
|
||||
time_unix:
|
||||
minimum: 0
|
||||
type: integer
|
||||
trace_id:
|
||||
type: string
|
||||
trace_state:
|
||||
type: string
|
||||
type: object
|
||||
TelemetrytypesFieldContext:
|
||||
enum:
|
||||
- metric
|
||||
@@ -6077,6 +5904,179 @@ components:
|
||||
TimeDuration:
|
||||
format: int64
|
||||
type: integer
|
||||
TracedetailtypesEvent:
|
||||
properties:
|
||||
attributeMap:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
isError:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
timeUnixNano:
|
||||
minimum: 0
|
||||
type: integer
|
||||
type: object
|
||||
TracedetailtypesGettableWaterfallTrace:
|
||||
properties:
|
||||
aggregations:
|
||||
items:
|
||||
$ref: '#/components/schemas/TracedetailtypesSpanAggregationResult'
|
||||
nullable: true
|
||||
type: array
|
||||
endTimestampMillis:
|
||||
minimum: 0
|
||||
type: integer
|
||||
hasMissingSpans:
|
||||
type: boolean
|
||||
hasMore:
|
||||
type: boolean
|
||||
rootServiceEntryPoint:
|
||||
type: string
|
||||
rootServiceName:
|
||||
type: string
|
||||
serviceNameToTotalDurationMap:
|
||||
additionalProperties:
|
||||
minimum: 0
|
||||
type: integer
|
||||
nullable: true
|
||||
type: object
|
||||
spans:
|
||||
items:
|
||||
$ref: '#/components/schemas/TracedetailtypesWaterfallSpan'
|
||||
nullable: true
|
||||
type: array
|
||||
startTimestampMillis:
|
||||
minimum: 0
|
||||
type: integer
|
||||
totalErrorSpansCount:
|
||||
minimum: 0
|
||||
type: integer
|
||||
totalSpansCount:
|
||||
minimum: 0
|
||||
type: integer
|
||||
uncollapsedSpans:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
TracedetailtypesPostableWaterfall:
|
||||
properties:
|
||||
aggregations:
|
||||
items:
|
||||
$ref: '#/components/schemas/TracedetailtypesSpanAggregation'
|
||||
nullable: true
|
||||
type: array
|
||||
limit:
|
||||
minimum: 0
|
||||
type: integer
|
||||
selectedSpanId:
|
||||
type: string
|
||||
uncollapsedSpans:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
TracedetailtypesSpanAggregation:
|
||||
properties:
|
||||
aggregation:
|
||||
$ref: '#/components/schemas/TracedetailtypesSpanAggregationType'
|
||||
field:
|
||||
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
|
||||
type: object
|
||||
TracedetailtypesSpanAggregationResult:
|
||||
properties:
|
||||
aggregation:
|
||||
$ref: '#/components/schemas/TracedetailtypesSpanAggregationType'
|
||||
field:
|
||||
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
|
||||
value:
|
||||
additionalProperties:
|
||||
minimum: 0
|
||||
type: integer
|
||||
nullable: true
|
||||
type: object
|
||||
type: object
|
||||
TracedetailtypesSpanAggregationType:
|
||||
enum:
|
||||
- span_count
|
||||
- execution_time_percentage
|
||||
- duration
|
||||
type: string
|
||||
TracedetailtypesWaterfallSpan:
|
||||
properties:
|
||||
attributes:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
type: object
|
||||
db_name:
|
||||
type: string
|
||||
db_operation:
|
||||
type: string
|
||||
duration_nano:
|
||||
minimum: 0
|
||||
type: integer
|
||||
events:
|
||||
items:
|
||||
$ref: '#/components/schemas/TracedetailtypesEvent'
|
||||
nullable: true
|
||||
type: array
|
||||
external_http_method:
|
||||
type: string
|
||||
external_http_url:
|
||||
type: string
|
||||
flags:
|
||||
minimum: 0
|
||||
type: integer
|
||||
has_children:
|
||||
type: boolean
|
||||
has_error:
|
||||
type: boolean
|
||||
http_host:
|
||||
type: string
|
||||
http_method:
|
||||
type: string
|
||||
http_url:
|
||||
type: string
|
||||
is_remote:
|
||||
type: string
|
||||
kind_string:
|
||||
type: string
|
||||
level:
|
||||
minimum: 0
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
parent_span_id:
|
||||
type: string
|
||||
resource:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
response_status_code:
|
||||
type: string
|
||||
span_id:
|
||||
type: string
|
||||
status_code:
|
||||
type: integer
|
||||
status_code_string:
|
||||
type: string
|
||||
status_message:
|
||||
type: string
|
||||
sub_tree_node_count:
|
||||
minimum: 0
|
||||
type: integer
|
||||
time_unix:
|
||||
minimum: 0
|
||||
type: integer
|
||||
trace_id:
|
||||
type: string
|
||||
trace_state:
|
||||
type: string
|
||||
type: object
|
||||
TypesAlertStatus:
|
||||
properties:
|
||||
inhibitedBy:
|
||||
@@ -18896,7 +18896,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpantypesPostableWaterfall'
|
||||
$ref: '#/components/schemas/TracedetailtypesPostableWaterfall'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
@@ -18904,7 +18904,7 @@ paths:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SpantypesGettableWaterfallTrace'
|
||||
$ref: '#/components/schemas/TracedetailtypesGettableWaterfallTrace'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
|
||||
@@ -6655,28 +6655,6 @@ export interface ServiceaccounttypesUpdatableFactorAPIKeyDTO {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type SpantypesEventDTOAttributeMap = { [key: string]: unknown };
|
||||
|
||||
export interface SpantypesEventDTO {
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
attributeMap?: SpantypesEventDTOAttributeMap;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
isError?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
timeUnixNano?: number;
|
||||
}
|
||||
|
||||
export enum SpantypesFieldContextDTO {
|
||||
attribute = 'attribute',
|
||||
resource = 'resource',
|
||||
@@ -6743,232 +6721,6 @@ export interface SpantypesGettableSpanMapperGroupsDTO {
|
||||
items: SpantypesSpanMapperGroupDTO[];
|
||||
}
|
||||
|
||||
export type SpantypesGettableWaterfallTraceDTOServiceNameToTotalDurationMapAnyOf =
|
||||
{ [key: string]: number };
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesGettableWaterfallTraceDTOServiceNameToTotalDurationMap =
|
||||
SpantypesGettableWaterfallTraceDTOServiceNameToTotalDurationMapAnyOf | null;
|
||||
|
||||
export enum SpantypesSpanAggregationTypeDTO {
|
||||
span_count = 'span_count',
|
||||
execution_time_percentage = 'execution_time_percentage',
|
||||
duration = 'duration',
|
||||
}
|
||||
export type SpantypesSpanAggregationResultDTOValueAnyOf = {
|
||||
[key: string]: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesSpanAggregationResultDTOValue =
|
||||
SpantypesSpanAggregationResultDTOValueAnyOf | null;
|
||||
|
||||
export interface SpantypesSpanAggregationResultDTO {
|
||||
aggregation?: SpantypesSpanAggregationTypeDTO;
|
||||
field?: TelemetrytypesTelemetryFieldKeyDTO;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
value?: SpantypesSpanAggregationResultDTOValue;
|
||||
}
|
||||
|
||||
export type SpantypesWaterfallSpanDTOAttributesAnyOf = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesWaterfallSpanDTOAttributes =
|
||||
SpantypesWaterfallSpanDTOAttributesAnyOf | null;
|
||||
|
||||
export type SpantypesWaterfallSpanDTOResourceAnyOf = { [key: string]: string };
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesWaterfallSpanDTOResource =
|
||||
SpantypesWaterfallSpanDTOResourceAnyOf | null;
|
||||
|
||||
export interface SpantypesWaterfallSpanDTO {
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
attributes?: SpantypesWaterfallSpanDTOAttributes;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
db_name?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
db_operation?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
duration_nano?: number;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
events?: SpantypesEventDTO[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
external_http_method?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
external_http_url?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
flags?: number;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
has_children?: boolean;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
has_error?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
http_host?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
http_method?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
http_url?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
is_remote?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
kind_string?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
level?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
parent_span_id?: string;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
resource?: SpantypesWaterfallSpanDTOResource;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
response_status_code?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
span_id?: string;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
status_code?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status_code_string?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status_message?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
sub_tree_node_count?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
time_unix?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
trace_id?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
trace_state?: string;
|
||||
}
|
||||
|
||||
export interface SpantypesGettableWaterfallTraceDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
aggregations?: SpantypesSpanAggregationResultDTO[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
endTimestampMillis?: number;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
hasMissingSpans?: boolean;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
hasMore?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
rootServiceEntryPoint?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
rootServiceName?: string;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
serviceNameToTotalDurationMap?: SpantypesGettableWaterfallTraceDTOServiceNameToTotalDurationMap;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
spans?: SpantypesWaterfallSpanDTO[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
startTimestampMillis?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
totalErrorSpansCount?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
totalSpansCount?: number;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
uncollapsedSpans?: string[] | null;
|
||||
}
|
||||
|
||||
export enum SpantypesSpanMapperOperationDTO {
|
||||
move = 'move',
|
||||
copy = 'copy',
|
||||
@@ -7018,31 +6770,6 @@ export interface SpantypesPostableSpanMapperGroupDTO {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SpantypesSpanAggregationDTO {
|
||||
aggregation?: SpantypesSpanAggregationTypeDTO;
|
||||
field?: TelemetrytypesTelemetryFieldKeyDTO;
|
||||
}
|
||||
|
||||
export interface SpantypesPostableWaterfallDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
aggregations?: SpantypesSpanAggregationDTO[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
selectedSpanId?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
uncollapsedSpans?: string[] | null;
|
||||
}
|
||||
|
||||
export interface SpantypesSpanMapperDTO {
|
||||
config: SpantypesSpanMapperConfigDTO;
|
||||
/**
|
||||
@@ -7151,6 +6878,281 @@ export interface TelemetrytypesGettableFieldValuesDTO {
|
||||
values: TelemetrytypesTelemetryFieldValuesDTO;
|
||||
}
|
||||
|
||||
export type TracedetailtypesEventDTOAttributeMap = { [key: string]: unknown };
|
||||
|
||||
export interface TracedetailtypesEventDTO {
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
attributeMap?: TracedetailtypesEventDTOAttributeMap;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
isError?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
timeUnixNano?: number;
|
||||
}
|
||||
|
||||
export type TracedetailtypesGettableWaterfallTraceDTOServiceNameToTotalDurationMapAnyOf =
|
||||
{ [key: string]: number };
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type TracedetailtypesGettableWaterfallTraceDTOServiceNameToTotalDurationMap =
|
||||
TracedetailtypesGettableWaterfallTraceDTOServiceNameToTotalDurationMapAnyOf | null;
|
||||
|
||||
export enum TracedetailtypesSpanAggregationTypeDTO {
|
||||
span_count = 'span_count',
|
||||
execution_time_percentage = 'execution_time_percentage',
|
||||
duration = 'duration',
|
||||
}
|
||||
export type TracedetailtypesSpanAggregationResultDTOValueAnyOf = {
|
||||
[key: string]: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type TracedetailtypesSpanAggregationResultDTOValue =
|
||||
TracedetailtypesSpanAggregationResultDTOValueAnyOf | null;
|
||||
|
||||
export interface TracedetailtypesSpanAggregationResultDTO {
|
||||
aggregation?: TracedetailtypesSpanAggregationTypeDTO;
|
||||
field?: TelemetrytypesTelemetryFieldKeyDTO;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
value?: TracedetailtypesSpanAggregationResultDTOValue;
|
||||
}
|
||||
|
||||
export type TracedetailtypesWaterfallSpanDTOAttributesAnyOf = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type TracedetailtypesWaterfallSpanDTOAttributes =
|
||||
TracedetailtypesWaterfallSpanDTOAttributesAnyOf | null;
|
||||
|
||||
export type TracedetailtypesWaterfallSpanDTOResourceAnyOf = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type TracedetailtypesWaterfallSpanDTOResource =
|
||||
TracedetailtypesWaterfallSpanDTOResourceAnyOf | null;
|
||||
|
||||
export interface TracedetailtypesWaterfallSpanDTO {
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
attributes?: TracedetailtypesWaterfallSpanDTOAttributes;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
db_name?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
db_operation?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
duration_nano?: number;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
events?: TracedetailtypesEventDTO[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
external_http_method?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
external_http_url?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
flags?: number;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
has_children?: boolean;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
has_error?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
http_host?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
http_method?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
http_url?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
is_remote?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
kind_string?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
level?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
parent_span_id?: string;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
resource?: TracedetailtypesWaterfallSpanDTOResource;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
response_status_code?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
span_id?: string;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
status_code?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status_code_string?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status_message?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
sub_tree_node_count?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
time_unix?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
trace_id?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
trace_state?: string;
|
||||
}
|
||||
|
||||
export interface TracedetailtypesGettableWaterfallTraceDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
aggregations?: TracedetailtypesSpanAggregationResultDTO[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
endTimestampMillis?: number;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
hasMissingSpans?: boolean;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
hasMore?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
rootServiceEntryPoint?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
rootServiceName?: string;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
serviceNameToTotalDurationMap?: TracedetailtypesGettableWaterfallTraceDTOServiceNameToTotalDurationMap;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
spans?: TracedetailtypesWaterfallSpanDTO[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
startTimestampMillis?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
totalErrorSpansCount?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
totalSpansCount?: number;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
uncollapsedSpans?: string[] | null;
|
||||
}
|
||||
|
||||
export interface TracedetailtypesSpanAggregationDTO {
|
||||
aggregation?: TracedetailtypesSpanAggregationTypeDTO;
|
||||
field?: TelemetrytypesTelemetryFieldKeyDTO;
|
||||
}
|
||||
|
||||
export interface TracedetailtypesPostableWaterfallDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
aggregations?: TracedetailtypesSpanAggregationDTO[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
selectedSpanId?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
uncollapsedSpans?: string[] | null;
|
||||
}
|
||||
|
||||
export interface TypesChangePasswordRequestDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -9230,7 +9232,7 @@ export type GetWaterfallPathParameters = {
|
||||
traceID: string;
|
||||
};
|
||||
export type GetWaterfall200 = {
|
||||
data: SpantypesGettableWaterfallTraceDTO;
|
||||
data: TracedetailtypesGettableWaterfallTraceDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
|
||||
@@ -15,7 +15,7 @@ import type {
|
||||
GetWaterfall200,
|
||||
GetWaterfallPathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
SpantypesPostableWaterfallDTO,
|
||||
TracedetailtypesPostableWaterfallDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
@@ -27,14 +27,14 @@ import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
|
||||
*/
|
||||
export const getWaterfall = (
|
||||
{ traceID }: GetWaterfallPathParameters,
|
||||
spantypesPostableWaterfallDTO?: BodyType<SpantypesPostableWaterfallDTO>,
|
||||
tracedetailtypesPostableWaterfallDTO?: BodyType<TracedetailtypesPostableWaterfallDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetWaterfall200>({
|
||||
url: `/api/v3/traces/${traceID}/waterfall`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: spantypesPostableWaterfallDTO,
|
||||
data: tracedetailtypesPostableWaterfallDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
@@ -48,7 +48,7 @@ export const getGetWaterfallMutationOptions = <
|
||||
TError,
|
||||
{
|
||||
pathParams: GetWaterfallPathParameters;
|
||||
data?: BodyType<SpantypesPostableWaterfallDTO>;
|
||||
data?: BodyType<TracedetailtypesPostableWaterfallDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
@@ -57,7 +57,7 @@ export const getGetWaterfallMutationOptions = <
|
||||
TError,
|
||||
{
|
||||
pathParams: GetWaterfallPathParameters;
|
||||
data?: BodyType<SpantypesPostableWaterfallDTO>;
|
||||
data?: BodyType<TracedetailtypesPostableWaterfallDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
@@ -74,7 +74,7 @@ export const getGetWaterfallMutationOptions = <
|
||||
Awaited<ReturnType<typeof getWaterfall>>,
|
||||
{
|
||||
pathParams: GetWaterfallPathParameters;
|
||||
data?: BodyType<SpantypesPostableWaterfallDTO>;
|
||||
data?: BodyType<TracedetailtypesPostableWaterfallDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
@@ -89,7 +89,7 @@ export type GetWaterfallMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getWaterfall>>
|
||||
>;
|
||||
export type GetWaterfallMutationBody =
|
||||
| BodyType<SpantypesPostableWaterfallDTO>
|
||||
| BodyType<TracedetailtypesPostableWaterfallDTO>
|
||||
| undefined;
|
||||
export type GetWaterfallMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
@@ -105,7 +105,7 @@ export const useGetWaterfall = <
|
||||
TError,
|
||||
{
|
||||
pathParams: GetWaterfallPathParameters;
|
||||
data?: BodyType<SpantypesPostableWaterfallDTO>;
|
||||
data?: BodyType<TracedetailtypesPostableWaterfallDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
@@ -114,7 +114,7 @@ export const useGetWaterfall = <
|
||||
TError,
|
||||
{
|
||||
pathParams: GetWaterfallPathParameters;
|
||||
data?: BodyType<SpantypesPostableWaterfallDTO>;
|
||||
data?: BodyType<TracedetailtypesPostableWaterfallDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
|
||||
@@ -5,10 +5,7 @@ import cx from 'classnames';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import {
|
||||
getBodyDisplayString,
|
||||
getSanitizedLogBody,
|
||||
} from 'container/LogDetailedView/utils';
|
||||
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -199,7 +196,7 @@ function ListLogView({
|
||||
{updatedSelecedFields.some((field) => field.name === 'body') && (
|
||||
<LogGeneralField
|
||||
fieldKey="Log"
|
||||
fieldValue={getBodyDisplayString(logData.body)}
|
||||
fieldValue={flattenLogData.body}
|
||||
linesPerRow={linesPerRow}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
|
||||
@@ -116,17 +116,28 @@ jest.mock('hooks/useNotifications', (): unknown => ({
|
||||
}));
|
||||
|
||||
// mock theme hook
|
||||
jest.mock('hooks/useDarkMode', (): unknown => ({
|
||||
useThemeMode: (): {
|
||||
jest.mock('hooks/useDarkMode', (): unknown => {
|
||||
const useThemeModeMock = (): {
|
||||
setAutoSwitch: jest.Mock;
|
||||
setTheme: jest.Mock;
|
||||
toggleTheme: jest.Mock;
|
||||
theme: string;
|
||||
autoSwitch: boolean;
|
||||
} => ({
|
||||
setAutoSwitch: jest.fn(),
|
||||
setTheme: jest.fn(),
|
||||
toggleTheme: jest.fn(),
|
||||
theme: 'dark',
|
||||
}),
|
||||
}));
|
||||
autoSwitch: false,
|
||||
});
|
||||
return {
|
||||
__esModule: true,
|
||||
default: useThemeModeMock,
|
||||
useThemeMode: useThemeModeMock,
|
||||
useIsDarkMode: (): boolean => true,
|
||||
useSystemTheme: (): 'dark' | 'light' => 'dark',
|
||||
};
|
||||
});
|
||||
|
||||
// mock updateUserPreference API and react-query mutation
|
||||
jest.mock('api/v1/user/preferences/name/update', (): jest.Mock => jest.fn());
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '@signozhq/ui/command';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useThemeMode } from 'hooks/useDarkMode';
|
||||
import { useThemeSelection } from 'hooks/useDarkMode/useThemeSelection';
|
||||
import history from 'lib/history';
|
||||
import { ROLES as UserRole } from 'types/roles';
|
||||
|
||||
@@ -36,7 +37,8 @@ export function CmdKPalette({
|
||||
}): JSX.Element | null {
|
||||
const { open, setOpen } = useCmdK();
|
||||
|
||||
const { setAutoSwitch, setTheme, theme } = useThemeMode();
|
||||
const { theme } = useThemeMode();
|
||||
const selectTheme = useThemeSelection();
|
||||
|
||||
// toggle palette with ⌘/Ctrl+K
|
||||
function handleGlobalCmdK(
|
||||
@@ -66,12 +68,10 @@ export function CmdKPalette({
|
||||
|
||||
function handleThemeChange(value: string): void {
|
||||
logEvent('Account Settings: Theme Changed', { theme: value });
|
||||
if (value === 'auto') {
|
||||
setAutoSwitch(true);
|
||||
} else {
|
||||
setAutoSwitch(false);
|
||||
setTheme(value);
|
||||
}
|
||||
// Close the palette inside the same flushSync batch as the theme change
|
||||
// so its dismissal is part of the captured "new" frame of the wipe;
|
||||
// otherwise the dialog would be visible in both snapshots and flicker.
|
||||
selectTheme(value, () => setOpen(false));
|
||||
}
|
||||
|
||||
function onClickHandler(key: string): void {
|
||||
|
||||
@@ -263,7 +263,7 @@ function External(): JSX.Element {
|
||||
timestamp: selectedTimeStamp,
|
||||
domainName: selectedData?.address || '',
|
||||
isError: true,
|
||||
stepInterval,
|
||||
stepInterval: 300,
|
||||
safeNavigate,
|
||||
})}
|
||||
/>
|
||||
@@ -306,7 +306,7 @@ function External(): JSX.Element {
|
||||
timestamp: selectedTimeStamp,
|
||||
domainName: selectedData?.address,
|
||||
isError: false,
|
||||
stepInterval,
|
||||
stepInterval: 300,
|
||||
safeNavigate,
|
||||
})}
|
||||
/>
|
||||
@@ -352,7 +352,7 @@ function External(): JSX.Element {
|
||||
timestamp: selectedTimeStamp,
|
||||
domainName: selectedData?.address,
|
||||
isError: false,
|
||||
stepInterval,
|
||||
stepInterval: 300,
|
||||
safeNavigate,
|
||||
})}
|
||||
/>
|
||||
@@ -395,7 +395,7 @@ function External(): JSX.Element {
|
||||
timestamp: selectedTimeStamp,
|
||||
domainName: selectedData?.address,
|
||||
isError: false,
|
||||
stepInterval,
|
||||
stepInterval: 300,
|
||||
safeNavigate,
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -151,7 +151,7 @@ export function onViewAPIMonitoringPopupClick({
|
||||
safeNavigate,
|
||||
}: OnViewAPIMonitoringPopupClickProps): (e?: React.MouseEvent) => void {
|
||||
return (e?: React.MouseEvent): void => {
|
||||
const endTime = timestamp;
|
||||
const endTime = timestamp + (stepInterval || 60);
|
||||
const startTime = timestamp - (stepInterval || 60);
|
||||
const filters = {
|
||||
items: [
|
||||
|
||||
@@ -12,6 +12,8 @@ import APIError from 'types/api/error';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
|
||||
const toggleThemeFunction = jest.fn();
|
||||
const setThemeFunction = jest.fn();
|
||||
const setAutoSwitchFunction = jest.fn();
|
||||
const logEventFunction = jest.fn();
|
||||
const copyToClipboardFn = jest.fn();
|
||||
const editUserFn = jest.fn();
|
||||
@@ -56,9 +58,11 @@ jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: jest.fn(() => true),
|
||||
useSystemTheme: jest.fn(() => 'dark'),
|
||||
default: jest.fn(() => ({
|
||||
theme: 'dark',
|
||||
setTheme: setThemeFunction,
|
||||
toggleTheme: toggleThemeFunction,
|
||||
autoSwitch: false,
|
||||
setAutoSwitch: jest.fn(),
|
||||
setAutoSwitch: setAutoSwitchFunction,
|
||||
})),
|
||||
}));
|
||||
|
||||
@@ -134,7 +138,8 @@ describe('MySettings Flows', () => {
|
||||
fireEvent.click(lightOption);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toggleThemeFunction).toHaveBeenCalled();
|
||||
expect(setAutoSwitchFunction).toHaveBeenCalledWith(false);
|
||||
expect(setThemeFunction).toHaveBeenCalledWith('light');
|
||||
expect(logEventFunction).toHaveBeenCalledWith(
|
||||
'Account Settings: Theme Changed',
|
||||
{
|
||||
|
||||
@@ -8,6 +8,8 @@ import updateUserPreference from 'api/v1/user/preferences/name/update';
|
||||
import { AxiosError } from 'axios';
|
||||
import { USER_PREFERENCES } from 'constants/userPreferences';
|
||||
import useThemeMode, { useIsDarkMode, useSystemTheme } from 'hooks/useDarkMode';
|
||||
import { THEME_MODE } from 'hooks/useDarkMode/constant';
|
||||
import { useThemeSelection } from 'hooks/useDarkMode/useThemeSelection';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { MonitorCog, Moon, Sun } from '@signozhq/icons';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
@@ -23,9 +25,10 @@ import './MySettings.styles.scss';
|
||||
function MySettings(): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { userPreferences, updateUserPreferenceInContext } = useAppContext();
|
||||
const { toggleTheme, autoSwitch, setAutoSwitch } = useThemeMode();
|
||||
const { autoSwitch } = useThemeMode();
|
||||
const systemTheme = useSystemTheme();
|
||||
const { notifications } = useNotifications();
|
||||
const selectTheme = useThemeSelection();
|
||||
|
||||
const [sideNavPinned, setSideNavPinned] = useState(false);
|
||||
|
||||
@@ -58,7 +61,7 @@ function MySettings(): JSX.Element {
|
||||
<Moon data-testid="dark-theme-icon" size={12} /> Dark{' '}
|
||||
</div>
|
||||
),
|
||||
value: 'dark',
|
||||
value: THEME_MODE.DARK,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
@@ -69,7 +72,7 @@ function MySettings(): JSX.Element {
|
||||
</Tag>
|
||||
</div>
|
||||
),
|
||||
value: 'light',
|
||||
value: THEME_MODE.LIGHT,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
@@ -77,46 +80,29 @@ function MySettings(): JSX.Element {
|
||||
<MonitorCog size={12} data-testid="auto-theme-icon" /> System{' '}
|
||||
</div>
|
||||
),
|
||||
value: 'auto',
|
||||
value: THEME_MODE.SYSTEM,
|
||||
},
|
||||
];
|
||||
|
||||
const [theme, setTheme] = useState(() => {
|
||||
if (autoSwitch) {
|
||||
return 'auto';
|
||||
return THEME_MODE.SYSTEM;
|
||||
}
|
||||
return isDarkMode ? 'dark' : 'light';
|
||||
return isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT;
|
||||
});
|
||||
|
||||
const handleThemeChange = ({ target: { value } }: RadioChangeEvent): void => {
|
||||
logEvent('Account Settings: Theme Changed', {
|
||||
theme: value,
|
||||
});
|
||||
setTheme(value);
|
||||
|
||||
if (value === 'auto') {
|
||||
setAutoSwitch(true);
|
||||
} else {
|
||||
setAutoSwitch(false);
|
||||
// Only toggle if the current theme is different from the target
|
||||
const targetIsDark = value === 'dark';
|
||||
if (targetIsDark !== isDarkMode) {
|
||||
toggleTheme();
|
||||
}
|
||||
}
|
||||
const handleThemeChange = (event: RadioChangeEvent): void => {
|
||||
const { value } = event.target;
|
||||
logEvent('Account Settings: Theme Changed', { theme: value });
|
||||
selectTheme(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (autoSwitch) {
|
||||
setTheme('auto');
|
||||
setTheme(THEME_MODE.SYSTEM);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDarkMode) {
|
||||
setTheme('dark');
|
||||
} else {
|
||||
setTheme('light');
|
||||
}
|
||||
setTheme(isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT);
|
||||
}, [autoSwitch, isDarkMode]);
|
||||
|
||||
const handleSideNavPinnedChange = (checked: boolean): void => {
|
||||
|
||||
64
frontend/src/hooks/useDarkMode/useThemeSelection.ts
Normal file
64
frontend/src/hooks/useDarkMode/useThemeSelection.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
canAnimateThemeTransition,
|
||||
runThemeTransition,
|
||||
} from 'utils/themeTransition';
|
||||
|
||||
import useThemeMode, { useSystemTheme } from './index';
|
||||
import { THEME_MODE } from './constant';
|
||||
|
||||
type SelectTheme = (value: string, onApplied?: () => void) => void;
|
||||
|
||||
// Centralises the "apply a theme selection" flow used by MySettings and the
|
||||
// command palette: figures out whether the visible (dark↔light) theme is
|
||||
// actually flipping, applies the state change, and — when capable — wraps the
|
||||
// change in a left→right view-transition wipe.
|
||||
//
|
||||
// `value` is one of THEME_MODE.{LIGHT,DARK,SYSTEM}; `onApplied` runs inside the
|
||||
// same flushSync batch as the theme change (useful for, e.g., closing the
|
||||
// command palette so its dismissal is part of the captured "new" snapshot).
|
||||
export function useThemeSelection(): SelectTheme {
|
||||
const { theme, setTheme, setAutoSwitch } = useThemeMode();
|
||||
const systemTheme = useSystemTheme();
|
||||
|
||||
return useCallback<SelectTheme>(
|
||||
(value, onApplied) => {
|
||||
const currentIsDark = theme === THEME_MODE.DARK;
|
||||
|
||||
// When switching to SYSTEM, the visible theme flips iff the OS preference
|
||||
// differs from what we're currently rendering.
|
||||
const resolvedTargetIsDark =
|
||||
value === THEME_MODE.SYSTEM
|
||||
? systemTheme === THEME_MODE.DARK
|
||||
: value === THEME_MODE.DARK;
|
||||
const willFlipDarkMode = resolvedTargetIsDark !== currentIsDark;
|
||||
|
||||
const applyChange = (): void => {
|
||||
if (value === THEME_MODE.SYSTEM) {
|
||||
// Also push the resolved light/dark value through setTheme so the
|
||||
// View Transition snapshot reflects the new theme synchronously.
|
||||
// Otherwise the flip would only land via ThemeProvider's effect
|
||||
// (setAutoSwitch → re-render → effect → setThemeState), which
|
||||
// isn't guaranteed to run inside this flushSync batch and would
|
||||
// cause the wipe to capture old → old followed by a post-animation snap.
|
||||
setAutoSwitch(true);
|
||||
setTheme(resolvedTargetIsDark ? THEME_MODE.DARK : THEME_MODE.LIGHT);
|
||||
} else {
|
||||
setAutoSwitch(false);
|
||||
setTheme(value);
|
||||
}
|
||||
onApplied?.();
|
||||
};
|
||||
|
||||
if (!willFlipDarkMode || !canAnimateThemeTransition()) {
|
||||
applyChange();
|
||||
return;
|
||||
}
|
||||
|
||||
runThemeTransition(applyChange);
|
||||
},
|
||||
[theme, systemTheme, setTheme, setAutoSwitch],
|
||||
);
|
||||
}
|
||||
|
||||
export default useThemeSelection;
|
||||
@@ -826,3 +826,22 @@ body.ai-assistant-panel-open {
|
||||
:root {
|
||||
--input-focus-outline-width: 0;
|
||||
}
|
||||
|
||||
// Scoped to .theme-wipe-active (toggled on <html> in runThemeTransition) so
|
||||
// these overrides don't leak into any unrelated view transitions added later.
|
||||
// We disable the default UA crossfade so the JS-driven clip-path wipe is the
|
||||
// only visible effect, and stack the new snapshot above the old.
|
||||
html.theme-wipe-active {
|
||||
&::view-transition-old(root),
|
||||
&::view-transition-new(root) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
&::view-transition-old(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&::view-transition-new(root) {
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
122
frontend/src/utils/__tests__/themeTransition.test.ts
Normal file
122
frontend/src/utils/__tests__/themeTransition.test.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import {
|
||||
canAnimateThemeTransition,
|
||||
runThemeTransition,
|
||||
THEME_WIPE_ACTIVE_CLASS,
|
||||
} from '../themeTransition';
|
||||
|
||||
type StartVT = (cb: () => void) => {
|
||||
ready: Promise<void>;
|
||||
finished: Promise<void>;
|
||||
};
|
||||
|
||||
const installStartViewTransition = (impl?: StartVT): jest.Mock => {
|
||||
const defaultImpl: StartVT = (cb) => {
|
||||
cb();
|
||||
return { ready: Promise.resolve(), finished: Promise.resolve() };
|
||||
};
|
||||
const fn = jest.fn(impl ?? defaultImpl);
|
||||
Object.defineProperty(document, 'startViewTransition', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: fn,
|
||||
});
|
||||
return fn;
|
||||
};
|
||||
|
||||
const removeStartViewTransition = (): void => {
|
||||
Object.defineProperty(document, 'startViewTransition', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const setReducedMotion = (matches: boolean): void => {
|
||||
(window.matchMedia as jest.Mock) = jest
|
||||
.fn()
|
||||
.mockImplementation((query: string) => ({
|
||||
matches: query === '(prefers-reduced-motion: reduce)' ? matches : false,
|
||||
media: query,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
}));
|
||||
};
|
||||
|
||||
describe('canAnimateThemeTransition', () => {
|
||||
afterEach(() => {
|
||||
removeStartViewTransition();
|
||||
});
|
||||
|
||||
it('returns false when document.startViewTransition is unavailable', () => {
|
||||
removeStartViewTransition();
|
||||
setReducedMotion(false);
|
||||
expect(canAnimateThemeTransition()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when prefers-reduced-motion is reduce', () => {
|
||||
installStartViewTransition();
|
||||
setReducedMotion(true);
|
||||
expect(canAnimateThemeTransition()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when API is supported and motion is allowed', () => {
|
||||
installStartViewTransition();
|
||||
setReducedMotion(false);
|
||||
expect(canAnimateThemeTransition()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('runThemeTransition', () => {
|
||||
afterEach(() => {
|
||||
removeStartViewTransition();
|
||||
document.documentElement.classList.remove(THEME_WIPE_ACTIVE_CLASS);
|
||||
});
|
||||
|
||||
it('falls back to running applyChange directly when API is missing', () => {
|
||||
removeStartViewTransition();
|
||||
const applyChange = jest.fn();
|
||||
runThemeTransition(applyChange);
|
||||
expect(applyChange).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
document.documentElement.classList.contains(THEME_WIPE_ACTIVE_CLASS),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('invokes startViewTransition and runs applyChange inside its callback', () => {
|
||||
const startVT = installStartViewTransition();
|
||||
const applyChange = jest.fn();
|
||||
runThemeTransition(applyChange);
|
||||
expect(startVT).toHaveBeenCalledTimes(1);
|
||||
expect(applyChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('toggles the wipe-active class on <html> for the lifetime of the transition', async () => {
|
||||
let resolveFinished: () => void = (): void => {};
|
||||
installStartViewTransition((cb) => {
|
||||
cb();
|
||||
return {
|
||||
ready: Promise.resolve(),
|
||||
finished: new Promise<void>((resolve) => {
|
||||
resolveFinished = resolve;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
runThemeTransition(() => undefined);
|
||||
|
||||
expect(
|
||||
document.documentElement.classList.contains(THEME_WIPE_ACTIVE_CLASS),
|
||||
).toBe(true);
|
||||
|
||||
resolveFinished();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(
|
||||
document.documentElement.classList.contains(THEME_WIPE_ACTIVE_CLASS),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
67
frontend/src/utils/themeTransition.ts
Normal file
67
frontend/src/utils/themeTransition.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { flushSync } from 'react-dom';
|
||||
|
||||
const WIPE_DURATION_MS = 400;
|
||||
const WIPE_EASING = 'ease-out';
|
||||
|
||||
// Toggled on <html> for the duration of the wipe so the CSS overrides
|
||||
// (animation: none on ::view-transition-{old,new}(root)) don't leak into
|
||||
// any future, unrelated view transitions in the app.
|
||||
export const THEME_WIPE_ACTIVE_CLASS = 'theme-wipe-active';
|
||||
|
||||
type ViewTransition = {
|
||||
ready: Promise<void>;
|
||||
finished: Promise<void>;
|
||||
};
|
||||
type DocumentWithVT = Document & {
|
||||
startViewTransition?: (callback: () => void) => ViewTransition;
|
||||
};
|
||||
|
||||
export function canAnimateThemeTransition(): boolean {
|
||||
const doc = document as DocumentWithVT;
|
||||
if (typeof doc.startViewTransition !== 'function') {
|
||||
return false;
|
||||
}
|
||||
return !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
}
|
||||
|
||||
// Runs `applyChange` inside a View Transition and wipes the new theme in from
|
||||
// left to right via a polygon clip-path on ::view-transition-new(root).
|
||||
// Callers should gate on canAnimateThemeTransition() first; this is a safe
|
||||
// no-animation fallback otherwise.
|
||||
export function runThemeTransition(applyChange: () => void): void {
|
||||
const doc = document as DocumentWithVT;
|
||||
if (!doc.startViewTransition) {
|
||||
applyChange();
|
||||
return;
|
||||
}
|
||||
|
||||
const root = document.documentElement;
|
||||
root.classList.add(THEME_WIPE_ACTIVE_CLASS);
|
||||
|
||||
const transition = doc.startViewTransition(() => {
|
||||
flushSync(applyChange);
|
||||
});
|
||||
|
||||
const from = 'polygon(0 0, 0 0, 0 100%, 0 100%)';
|
||||
const to = 'polygon(0 0, 100% 0, 100% 100%, 0 100%)';
|
||||
|
||||
transition.ready
|
||||
.then(() =>
|
||||
root.animate(
|
||||
{ clipPath: [from, to] },
|
||||
{
|
||||
duration: WIPE_DURATION_MS,
|
||||
easing: WIPE_EASING,
|
||||
pseudoElement: '::view-transition-new(root)',
|
||||
},
|
||||
),
|
||||
)
|
||||
.catch(() => {
|
||||
// Transition cancelled — applyChange has already run.
|
||||
});
|
||||
|
||||
const cleanup = (): void => {
|
||||
root.classList.remove(THEME_WIPE_ACTIVE_CLASS);
|
||||
};
|
||||
transition.finished.then(cleanup).catch(cleanup);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/spantypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/tracedetailtypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
@@ -17,9 +17,9 @@ func (provider *provider) addTraceDetailRoutes(router *mux.Router) error {
|
||||
Tags: []string{"tracedetail"},
|
||||
Summary: "Get waterfall view for a trace",
|
||||
Description: "Returns the waterfall view of spans for a given trace ID with tree structure, metadata, and windowed pagination",
|
||||
Request: new(spantypes.PostableWaterfall),
|
||||
Request: new(tracedetailtypes.PostableWaterfall),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(spantypes.GettableWaterfallTrace),
|
||||
Response: new(tracedetailtypes.GettableWaterfallTrace),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
|
||||
@@ -2,11 +2,8 @@ package implspanmapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
"github.com/SigNoz/signoz/pkg/types/opamptypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/spantypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
@@ -37,22 +34,11 @@ func (module *module) UpdateGroup(ctx context.Context, orgID, id valuer.UUID, na
|
||||
return err
|
||||
}
|
||||
group.Update(name, condition, enabled, updatedBy)
|
||||
|
||||
err = module.store.UpdateGroup(ctx, group)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
agentConf.NotifyConfigUpdate(ctx)
|
||||
return nil
|
||||
return module.store.UpdateGroup(ctx, group)
|
||||
}
|
||||
|
||||
func (module *module) DeleteGroup(ctx context.Context, orgID, id valuer.UUID) error {
|
||||
err := module.store.DeleteGroup(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
agentConf.NotifyConfigUpdate(ctx)
|
||||
return nil
|
||||
return module.store.DeleteGroup(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (module *module) ListMappers(ctx context.Context, orgID, groupID valuer.UUID) ([]*spantypes.SpanMapper, error) {
|
||||
@@ -68,12 +54,7 @@ func (module *module) CreateMapper(ctx context.Context, orgID, groupID valuer.UU
|
||||
if _, err := module.store.GetGroup(ctx, orgID, groupID); err != nil {
|
||||
return err
|
||||
}
|
||||
err := module.store.CreateMapper(ctx, mapper)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
agentConf.NotifyConfigUpdate(ctx)
|
||||
return nil
|
||||
return module.store.CreateMapper(ctx, mapper)
|
||||
}
|
||||
|
||||
func (module *module) UpdateMapper(ctx context.Context, orgID, groupID, id valuer.UUID, fieldContext spantypes.FieldContext, config *spantypes.SpanMapperConfig, enabled *bool, updatedBy string) error {
|
||||
@@ -85,72 +66,9 @@ func (module *module) UpdateMapper(ctx context.Context, orgID, groupID, id value
|
||||
return err
|
||||
}
|
||||
mapper.Update(fieldContext, config, enabled, updatedBy)
|
||||
err = module.store.UpdateMapper(ctx, mapper)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
agentConf.NotifyConfigUpdate(ctx)
|
||||
return nil
|
||||
return module.store.UpdateMapper(ctx, mapper)
|
||||
}
|
||||
|
||||
func (module *module) DeleteMapper(ctx context.Context, orgID, groupID, id valuer.UUID) error {
|
||||
err := module.store.DeleteMapper(ctx, orgID, groupID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
agentConf.NotifyConfigUpdate(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (module *module) AgentFeatureType() agentConf.AgentFeatureType {
|
||||
return spantypes.SpanAttrMappingFeatureType
|
||||
}
|
||||
|
||||
func (module *module) RecommendAgentConfig(orgID valuer.UUID, currentConfYaml []byte, configVersion *opamptypes.AgentConfigVersion) ([]byte, string, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
enabled, err := module.listEnabledGroupsWithMappers(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
updatedConf, err := spantypes.GenerateCollectorConfigWithSpanMapperProcessor(currentConfYaml, enabled)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
serialized, err := json.Marshal(enabled)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return updatedConf, string(serialized), nil
|
||||
}
|
||||
|
||||
// listEnabledGroupsWithMappers returns groups with their mappers.
|
||||
func (module *module) listEnabledGroupsWithMappers(ctx context.Context, orgID valuer.UUID) ([]*spantypes.SpanMapperGroupWithMappers, error) {
|
||||
enabled := true
|
||||
groups, err := module.store.ListGroups(ctx, orgID, &spantypes.ListSpanMapperGroupsQuery{Enabled: &enabled})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make([]*spantypes.SpanMapperGroupWithMappers, 0, len(groups))
|
||||
for _, g := range groups {
|
||||
mappers, err := module.store.ListMappers(ctx, orgID, g.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
enabledMappers := make([]*spantypes.SpanMapper, 0, len(mappers))
|
||||
for _, m := range mappers {
|
||||
if m.Enabled {
|
||||
enabledMappers = append(enabledMappers, m)
|
||||
}
|
||||
}
|
||||
if len(enabledMappers) == 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, &spantypes.SpanMapperGroupWithMappers{Group: g, Mappers: enabledMappers})
|
||||
}
|
||||
return out, nil
|
||||
return module.store.DeleteMapper(ctx, orgID, groupID, id)
|
||||
}
|
||||
|
||||
@@ -4,16 +4,12 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
"github.com/SigNoz/signoz/pkg/types/spantypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// Module defines the business logic for span attribute mapping groups and mappers.
|
||||
type Module interface {
|
||||
// Since this module interacts with OpAMP, it must implement the AgentFeature interface.
|
||||
agentConf.AgentFeature
|
||||
|
||||
// Group operations
|
||||
ListGroups(ctx context.Context, orgID valuer.UUID, q *spantypes.ListSpanMapperGroupsQuery) ([]*spantypes.SpanMapperGroup, error)
|
||||
GetGroup(ctx context.Context, orgID, id valuer.UUID) (*spantypes.SpanMapperGroup, error)
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/http/binding"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/types/spantypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/tracedetailtypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ func NewHandler(module tracedetail.Module) tracedetail.Handler {
|
||||
}
|
||||
|
||||
func (h *handler) GetWaterfall(rw http.ResponseWriter, r *http.Request) {
|
||||
req := new(spantypes.PostableWaterfall)
|
||||
req := new(tracedetailtypes.PostableWaterfall)
|
||||
if err := binding.JSON.BindBody(r.Body, req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
|
||||
@@ -5,16 +5,16 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/types/spantypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/tracedetailtypes"
|
||||
)
|
||||
|
||||
type module struct {
|
||||
store spantypes.TraceStore
|
||||
store tracedetailtypes.TraceStore
|
||||
settings factory.ScopedProviderSettings
|
||||
config tracedetail.Config
|
||||
}
|
||||
|
||||
func NewModule(traceStore spantypes.TraceStore, providerSettings factory.ProviderSettings, cfg tracedetail.Config) *module {
|
||||
func NewModule(traceStore tracedetailtypes.TraceStore, providerSettings factory.ProviderSettings, cfg tracedetail.Config) *module {
|
||||
scopedProviderSettings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/tracedetail/impltracedetail")
|
||||
return &module{
|
||||
config: cfg,
|
||||
@@ -23,7 +23,7 @@ func NewModule(traceStore spantypes.TraceStore, providerSettings factory.Provide
|
||||
}
|
||||
}
|
||||
|
||||
func (m *module) GetWaterfall(ctx context.Context, traceID string, req *spantypes.PostableWaterfall) (*spantypes.GettableWaterfallTrace, error) {
|
||||
func (m *module) GetWaterfall(ctx context.Context, traceID string, req *tracedetailtypes.PostableWaterfall) (*tracedetailtypes.GettableWaterfallTrace, error) {
|
||||
waterfallTrace, err := m.getTraceData(ctx, traceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -37,16 +37,16 @@ func (m *module) GetWaterfall(ctx context.Context, traceID string, req *spantype
|
||||
m.config.Waterfall.MaxDepthToAutoExpand,
|
||||
)
|
||||
|
||||
aggregationResults := make([]spantypes.SpanAggregationResult, 0, len(req.Aggregations))
|
||||
aggregationResults := make([]tracedetailtypes.SpanAggregationResult, 0, len(req.Aggregations))
|
||||
for _, a := range req.Aggregations {
|
||||
aggregationResults = append(aggregationResults, waterfallTrace.GetSpanAggregation(a.Aggregation, a.Field))
|
||||
}
|
||||
|
||||
return spantypes.NewGettableWaterfallTrace(waterfallTrace, selectedSpans, uncollapsedSpans, selectedAllSpans, aggregationResults), nil
|
||||
return tracedetailtypes.NewGettableWaterfallTrace(waterfallTrace, selectedSpans, uncollapsedSpans, selectedAllSpans, aggregationResults), nil
|
||||
}
|
||||
|
||||
// getTraceData returns the waterfall cache for the given traceID with fallback on DB.
|
||||
func (m *module) getTraceData(ctx context.Context, traceID string) (*spantypes.WaterfallTrace, error) {
|
||||
func (m *module) getTraceData(ctx context.Context, traceID string) (*tracedetailtypes.WaterfallTrace, error) {
|
||||
summary, err := m.store.GetTraceSummary(ctx, traceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -58,9 +58,9 @@ func (m *module) getTraceData(ctx context.Context, traceID string) (*spantypes.W
|
||||
}
|
||||
|
||||
if len(spanItems) == 0 {
|
||||
return nil, spantypes.ErrTraceNotFound
|
||||
return nil, tracedetailtypes.ErrTraceNotFound
|
||||
}
|
||||
|
||||
traceData := spantypes.NewWaterfallTraceFromSpans(spanItems)
|
||||
traceData := tracedetailtypes.NewWaterfallTraceFromSpans(spanItems)
|
||||
return traceData, nil
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/spantypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/tracedetailtypes"
|
||||
)
|
||||
|
||||
type traceStore struct {
|
||||
@@ -20,28 +20,28 @@ func NewTraceStore(ts telemetrystore.TelemetryStore) *traceStore {
|
||||
return &traceStore{telemetryStore: ts}
|
||||
}
|
||||
|
||||
func (s *traceStore) GetTraceSummary(ctx context.Context, traceID string) (*spantypes.TraceSummary, error) {
|
||||
func (s *traceStore) GetTraceSummary(ctx context.Context, traceID string) (*tracedetailtypes.TraceSummary, error) {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
sb.Select("trace_id", "min(start) AS start", "max(end) AS end", "sum(num_spans) AS num_spans")
|
||||
sb.From(fmt.Sprintf("%s.%s", spantypes.TraceDB, spantypes.TraceSummaryTable))
|
||||
sb.From(fmt.Sprintf("%s.%s", tracedetailtypes.TraceDB, tracedetailtypes.TraceSummaryTable))
|
||||
sb.Where(sb.E("trace_id", traceID))
|
||||
sb.GroupBy("trace_id")
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
var summary spantypes.TraceSummary
|
||||
var summary tracedetailtypes.TraceSummary
|
||||
err := s.telemetryStore.ClickhouseDB().QueryRow(ctx, query, args...).Scan(
|
||||
&summary.TraceID, &summary.Start, &summary.End, &summary.NumSpans,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, spantypes.ErrTraceNotFound
|
||||
return nil, tracedetailtypes.ErrTraceNotFound
|
||||
}
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error querying trace summary")
|
||||
}
|
||||
return &summary, nil
|
||||
}
|
||||
|
||||
func (s *traceStore) GetTraceSpans(ctx context.Context, traceID string, summary *spantypes.TraceSummary) ([]spantypes.StorableSpan, error) {
|
||||
func (s *traceStore) GetTraceSpans(ctx context.Context, traceID string, summary *tracedetailtypes.TraceSummary) ([]tracedetailtypes.StorableSpan, error) {
|
||||
// DISTINCT ON (span_id) is ClickHouse-specific syntax not supported by sqlbuilder
|
||||
query := fmt.Sprintf(`
|
||||
SELECT DISTINCT ON (span_id)
|
||||
@@ -55,9 +55,9 @@ func (s *traceStore) GetTraceSpans(ctx context.Context, traceID string, summary
|
||||
FROM %s.%s
|
||||
WHERE trace_id=? AND ts_bucket_start>=? AND ts_bucket_start<=?
|
||||
ORDER BY timestamp ASC, name ASC`,
|
||||
spantypes.TraceDB, spantypes.TraceTable,
|
||||
tracedetailtypes.TraceDB, tracedetailtypes.TraceTable,
|
||||
)
|
||||
var spanItems []spantypes.StorableSpan
|
||||
var spanItems []tracedetailtypes.StorableSpan
|
||||
err := s.telemetryStore.ClickhouseDB().Select(
|
||||
ctx, &spanItems, query,
|
||||
traceID,
|
||||
|
||||
@@ -37,7 +37,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/spantypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/tracedetailtypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -45,8 +45,8 @@ import (
|
||||
// Helpers
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
func mkSpan(id, service string, children ...*spantypes.WaterfallSpan) *spantypes.WaterfallSpan {
|
||||
return &spantypes.WaterfallSpan{
|
||||
func mkSpan(id, service string, children ...*tracedetailtypes.WaterfallSpan) *tracedetailtypes.WaterfallSpan {
|
||||
return &tracedetailtypes.WaterfallSpan{
|
||||
SpanID: id,
|
||||
ServiceName: service,
|
||||
Name: id + "-op",
|
||||
@@ -54,7 +54,7 @@ func mkSpan(id, service string, children ...*spantypes.WaterfallSpan) *spantypes
|
||||
}
|
||||
}
|
||||
|
||||
func spanIDs(spans []*spantypes.WaterfallSpan) []string {
|
||||
func spanIDs(spans []*tracedetailtypes.WaterfallSpan) []string {
|
||||
ids := make([]string, len(spans))
|
||||
for i, s := range spans {
|
||||
ids[i] = s.SpanID
|
||||
@@ -62,10 +62,10 @@ func spanIDs(spans []*spantypes.WaterfallSpan) []string {
|
||||
return ids
|
||||
}
|
||||
|
||||
func buildSpanMap(roots ...*spantypes.WaterfallSpan) map[string]*spantypes.WaterfallSpan {
|
||||
m := map[string]*spantypes.WaterfallSpan{}
|
||||
var walk func(*spantypes.WaterfallSpan)
|
||||
walk = func(s *spantypes.WaterfallSpan) {
|
||||
func buildSpanMap(roots ...*tracedetailtypes.WaterfallSpan) map[string]*tracedetailtypes.WaterfallSpan {
|
||||
m := map[string]*tracedetailtypes.WaterfallSpan{}
|
||||
var walk func(*tracedetailtypes.WaterfallSpan)
|
||||
walk = func(s *tracedetailtypes.WaterfallSpan) {
|
||||
m[s.SpanID] = s
|
||||
for _, c := range s.Children {
|
||||
walk(c)
|
||||
@@ -80,8 +80,8 @@ func buildSpanMap(roots ...*spantypes.WaterfallSpan) map[string]*spantypes.Water
|
||||
}
|
||||
|
||||
// makeChain builds a linear trace: span0 → span1 → … → span(n-1).
|
||||
func makeChain(n int) (*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan, []string) {
|
||||
spans := make([]*spantypes.WaterfallSpan, n)
|
||||
func makeChain(n int) (*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan, []string) {
|
||||
spans := make([]*tracedetailtypes.WaterfallSpan, n)
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
if i == n-1 {
|
||||
spans[i] = mkSpan(fmt.Sprintf("span%d", i), "svc")
|
||||
@@ -96,8 +96,8 @@ func makeChain(n int) (*spantypes.WaterfallSpan, map[string]*spantypes.Waterfall
|
||||
return spans[0], buildSpanMap(spans[0]), uncollapsed
|
||||
}
|
||||
|
||||
func getWaterfallTrace(roots []*spantypes.WaterfallSpan, spanMap map[string]*spantypes.WaterfallSpan) *spantypes.WaterfallTrace {
|
||||
return spantypes.NewWaterfallTrace(0, 0, uint64(len(spanMap)), 0, spanMap, nil, roots, false)
|
||||
func getWaterfallTrace(roots []*tracedetailtypes.WaterfallSpan, spanMap map[string]*tracedetailtypes.WaterfallSpan) *tracedetailtypes.WaterfallTrace {
|
||||
return tracedetailtypes.NewWaterfallTrace(0, 0, uint64(len(spanMap)), 0, spanMap, nil, roots, false)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -107,7 +107,7 @@ func getWaterfallTrace(roots []*spantypes.WaterfallSpan, spanMap map[string]*spa
|
||||
func TestGetSelectedSpans_SpanOrdering(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
buildRoots func() ([]*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan)
|
||||
buildRoots func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan)
|
||||
uncollapsedSpans []string
|
||||
selectedSpanID string
|
||||
wantSpanIDs []string
|
||||
@@ -115,12 +115,12 @@ func TestGetSelectedSpans_SpanOrdering(t *testing.T) {
|
||||
{
|
||||
// Pre-order traversal is preserved: parent before children, siblings left-to-right.
|
||||
name: "pre_order_traversal",
|
||||
buildRoots: func() ([]*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
|
||||
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("child1", "svc", mkSpan("grandchild", "svc")),
|
||||
mkSpan("child2", "svc"),
|
||||
)
|
||||
return []*spantypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{"root", "child1"},
|
||||
selectedSpanID: "root",
|
||||
@@ -133,12 +133,12 @@ func TestGetSelectedSpans_SpanOrdering(t *testing.T) {
|
||||
// ├─ childA (uncollapsed) → grandchildA ✓
|
||||
// └─ childB (uncollapsed) → grandchildB ✓
|
||||
name: "multiple_uncollapsed",
|
||||
buildRoots: func() ([]*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
|
||||
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("childA", "svc", mkSpan("grandchildA", "svc")),
|
||||
mkSpan("childB", "svc", mkSpan("grandchildB", "svc")),
|
||||
)
|
||||
return []*spantypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{"root", "childA", "childB"},
|
||||
selectedSpanID: "root",
|
||||
@@ -154,7 +154,7 @@ func TestGetSelectedSpans_SpanOrdering(t *testing.T) {
|
||||
// │ └─ grandchild2 ✓
|
||||
// └─ childB ← selected (not expanded)
|
||||
name: "manual_uncollapse",
|
||||
buildRoots: func() ([]*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
|
||||
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("childA", "svc",
|
||||
mkSpan("grandchild1", "svc", mkSpan("greatGrandchild", "svc")),
|
||||
@@ -162,7 +162,7 @@ func TestGetSelectedSpans_SpanOrdering(t *testing.T) {
|
||||
),
|
||||
mkSpan("childB", "svc"),
|
||||
)
|
||||
return []*spantypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{"childA"},
|
||||
selectedSpanID: "childB",
|
||||
@@ -171,12 +171,12 @@ func TestGetSelectedSpans_SpanOrdering(t *testing.T) {
|
||||
{
|
||||
// A collapsed span hides all children.
|
||||
name: "collapsed_span_hides_children",
|
||||
buildRoots: func() ([]*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
|
||||
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("child1", "svc"),
|
||||
mkSpan("child2", "svc"),
|
||||
)
|
||||
return []*spantypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{},
|
||||
selectedSpanID: "root",
|
||||
@@ -187,13 +187,13 @@ func TestGetSelectedSpans_SpanOrdering(t *testing.T) {
|
||||
//
|
||||
// root → parent → selected
|
||||
name: "path_to_selected_is_uncollapsed",
|
||||
buildRoots: func() ([]*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
|
||||
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("parent", "svc",
|
||||
mkSpan("selected", "svc"),
|
||||
),
|
||||
)
|
||||
return []*spantypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{},
|
||||
selectedSpanID: "selected",
|
||||
@@ -206,14 +206,14 @@ func TestGetSelectedSpans_SpanOrdering(t *testing.T) {
|
||||
// ├─ unrelated → unrelated-child (✗)
|
||||
// └─ parent → selected
|
||||
name: "siblings_not_expanded",
|
||||
buildRoots: func() ([]*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
|
||||
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("unrelated", "svc", mkSpan("unrelated-child", "svc")),
|
||||
mkSpan("parent", "svc",
|
||||
mkSpan("selected", "svc"),
|
||||
),
|
||||
)
|
||||
return []*spantypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{},
|
||||
selectedSpanID: "selected",
|
||||
@@ -223,9 +223,9 @@ func TestGetSelectedSpans_SpanOrdering(t *testing.T) {
|
||||
{
|
||||
// An unknown selectedSpanID must not panic; returns a window from index 0.
|
||||
name: "unknown_selected_span",
|
||||
buildRoots: func() ([]*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
|
||||
buildRoots: func() ([]*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc", mkSpan("child", "svc"))
|
||||
return []*spantypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
return []*tracedetailtypes.WaterfallSpan{root}, buildSpanMap(root)
|
||||
},
|
||||
uncollapsedSpans: []string{},
|
||||
selectedSpanID: "nonexistent",
|
||||
@@ -257,10 +257,10 @@ func TestGetSelectedSpans_MultipleRoots(t *testing.T) {
|
||||
root2 := mkSpan("root2", "svc-b", mkSpan("child2", "svc-b"))
|
||||
spanMap := buildSpanMap(root1, root2)
|
||||
|
||||
trace := getWaterfallTrace([]*spantypes.WaterfallSpan{root1, root2}, spanMap)
|
||||
trace := getWaterfallTrace([]*tracedetailtypes.WaterfallSpan{root1, root2}, spanMap)
|
||||
spans, _ := trace.GetSelectedSpans([]string{"root1", "root2"}, "root1", 500, 5)
|
||||
|
||||
traceRespnose := spantypes.NewGettableWaterfallTrace(trace, spans, nil, false, nil)
|
||||
traceRespnose := tracedetailtypes.NewGettableWaterfallTrace(trace, spans, nil, false, nil)
|
||||
|
||||
assert.Equal(t, []string{"root1", "child1", "root2", "child2"}, spanIDs(spans), "root1 subtree must precede root2 subtree")
|
||||
assert.Equal(t, "svc-a", traceRespnose.RootServiceName, "metadata comes from first root")
|
||||
@@ -274,7 +274,7 @@ func TestGetSelectedSpans_MultipleRoots(t *testing.T) {
|
||||
func TestGetSelectedSpans_UncollapsedTracking(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
buildRoot func() (*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan)
|
||||
buildRoot func() (*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan)
|
||||
uncollapsedSpans []string
|
||||
selectedSpanID string
|
||||
wantSpanIDs []string
|
||||
@@ -283,7 +283,7 @@ func TestGetSelectedSpans_UncollapsedTracking(t *testing.T) {
|
||||
{
|
||||
// The path-to-selected spans are returned in updatedUncollapsedSpans.
|
||||
name: "path_returned_in_uncollapsed",
|
||||
buildRoot: func() (*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
|
||||
buildRoot: func() (*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("parent", "svc",
|
||||
mkSpan("selected", "svc"),
|
||||
@@ -301,7 +301,7 @@ func TestGetSelectedSpans_UncollapsedTracking(t *testing.T) {
|
||||
{
|
||||
// Siblings of ancestors are not tracked as uncollapsed.
|
||||
name: "siblings_not_in_uncollapsed",
|
||||
buildRoot: func() (*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
|
||||
buildRoot: func() (*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("unrelated", "svc", mkSpan("unrelated-child", "svc")),
|
||||
mkSpan("parent", "svc",
|
||||
@@ -330,7 +330,7 @@ func TestGetSelectedSpans_UncollapsedTracking(t *testing.T) {
|
||||
// └─ grandchildB (internal ✓)
|
||||
// └─ leafB (leaf ✗)
|
||||
name: "auto_expanded_spans_returned",
|
||||
buildRoot: func() (*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
|
||||
buildRoot: func() (*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("childA", "svc",
|
||||
mkSpan("grandchildA", "svc",
|
||||
@@ -361,7 +361,7 @@ func TestGetSelectedSpans_UncollapsedTracking(t *testing.T) {
|
||||
// If the selected span is already in uncollapsedSpans,
|
||||
// it should appear exactly once in the result.
|
||||
name: "duplicate_in_uncollapsed",
|
||||
buildRoot: func() (*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
|
||||
buildRoot: func() (*tracedetailtypes.WaterfallSpan, map[string]*tracedetailtypes.WaterfallSpan) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("selected", "svc", mkSpan("child", "svc")),
|
||||
)
|
||||
@@ -384,7 +384,7 @@ func TestGetSelectedSpans_UncollapsedTracking(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
root, spanMap := tc.buildRoot()
|
||||
trace := getWaterfallTrace([]*spantypes.WaterfallSpan{root}, spanMap)
|
||||
trace := getWaterfallTrace([]*tracedetailtypes.WaterfallSpan{root}, spanMap)
|
||||
spans, uncollapsed := trace.GetSelectedSpans(tc.uncollapsedSpans, tc.selectedSpanID, 500, 5)
|
||||
if tc.wantSpanIDs != nil {
|
||||
assert.Equal(t, tc.wantSpanIDs, spanIDs(spans))
|
||||
@@ -412,10 +412,10 @@ func TestGetSelectedSpans_SpanMetadata(t *testing.T) {
|
||||
mkSpan("child2", "svc"),
|
||||
)
|
||||
spanMap := buildSpanMap(root)
|
||||
trace := getWaterfallTrace([]*spantypes.WaterfallSpan{root}, spanMap)
|
||||
trace := getWaterfallTrace([]*tracedetailtypes.WaterfallSpan{root}, spanMap)
|
||||
spans, _ := trace.GetSelectedSpans([]string{"root", "child1"}, "root", 500, 5)
|
||||
|
||||
byID := map[string]*spantypes.WaterfallSpan{}
|
||||
byID := map[string]*tracedetailtypes.WaterfallSpan{}
|
||||
for _, s := range spans {
|
||||
byID[s.SpanID] = s
|
||||
}
|
||||
@@ -478,7 +478,7 @@ func TestGetSelectedSpans_Window(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
root, spanMap, uncollapsed := makeChain(600)
|
||||
trace := getWaterfallTrace([]*spantypes.WaterfallSpan{root}, spanMap)
|
||||
trace := getWaterfallTrace([]*tracedetailtypes.WaterfallSpan{root}, spanMap)
|
||||
spans, _ := trace.GetSelectedSpans(uncollapsed, tc.selectedSpanID, 500, 5)
|
||||
|
||||
assert.Equal(t, tc.wantLen, len(spans), "window size")
|
||||
@@ -536,7 +536,7 @@ func TestGetSelectedSpans_DepthCountedFromSelectedSpan(t *testing.T) {
|
||||
root := mkSpan("root", "svc", mkSpan("A", "svc", selected))
|
||||
|
||||
spanMap := buildSpanMap(root)
|
||||
trace := getWaterfallTrace([]*spantypes.WaterfallSpan{root}, spanMap)
|
||||
trace := getWaterfallTrace([]*tracedetailtypes.WaterfallSpan{root}, spanMap)
|
||||
spans, _ := trace.GetSelectedSpans([]string{"selected"}, "selected", 500, 5)
|
||||
ids := spanIDs(spans)
|
||||
|
||||
@@ -565,9 +565,9 @@ func TestGetAllSpans(t *testing.T) {
|
||||
),
|
||||
),
|
||||
)
|
||||
trace := getWaterfallTrace([]*spantypes.WaterfallSpan{root}, nil)
|
||||
trace := getWaterfallTrace([]*tracedetailtypes.WaterfallSpan{root}, nil)
|
||||
spans := trace.GetAllSpans()
|
||||
traceResponse := spantypes.NewGettableWaterfallTrace(trace, spans, nil, true, nil)
|
||||
traceResponse := tracedetailtypes.NewGettableWaterfallTrace(trace, spans, nil, true, nil)
|
||||
assert.ElementsMatch(t, spanIDs(spans), []string{"root", "childA", "grandchildA", "leafA", "childB", "grandchildB", "leafB"})
|
||||
assert.Equal(t, "svc", traceResponse.RootServiceName)
|
||||
assert.Equal(t, "root-op", traceResponse.RootServiceEntryPoint)
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/spantypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/tracedetailtypes"
|
||||
)
|
||||
|
||||
// Handler exposes HTTP handlers for trace detail APIs.
|
||||
@@ -14,5 +14,5 @@ type Handler interface {
|
||||
|
||||
// Module defines the business logic for trace detail operations.
|
||||
type Module interface {
|
||||
GetWaterfall(ctx context.Context, traceID string, req *spantypes.PostableWaterfall) (*spantypes.GettableWaterfallTrace, error)
|
||||
GetWaterfall(ctx context.Context, traceID string, req *tracedetailtypes.PostableWaterfall) (*tracedetailtypes.GettableWaterfallTrace, error)
|
||||
}
|
||||
|
||||
@@ -13,9 +13,7 @@ var (
|
||||
ErrCodeMappingGroupAlreadyExists = errors.MustNewCode("span_attribute_mapping_group_already_exists")
|
||||
)
|
||||
|
||||
// SpanMapperGroupCondition gates whether a group's rules run for a given span.
|
||||
// A group runs when any attribute or resource key on the span CONTAINS one of
|
||||
// the listed substrings (plain substring match — no glob syntax).
|
||||
// A group runs when any of the listed attribute/resource key patterns match.
|
||||
type SpanMapperGroupCondition struct {
|
||||
Attributes []string `json:"attributes" required:"true" nullable:"true"`
|
||||
Resource []string `json:"resource" required:"true" nullable:"true"`
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
package spantypes
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
SpanAttrMappingFeatureType agentConf.AgentFeatureType = "span_attr_mapping"
|
||||
|
||||
ProcessorName = "signozspanmapper"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCodeInvalidCollectorConfig = errors.MustNewCode("invalid_collector_config")
|
||||
ErrCodeBuildMappingProcessorConfig = errors.MustNewCode("build_mapping_processor_config")
|
||||
)
|
||||
|
||||
type SpanMapperGroupWithMappers struct {
|
||||
Group *SpanMapperGroup `json:"group"`
|
||||
Mappers []*SpanMapper `json:"mappers"`
|
||||
}
|
||||
|
||||
// spanMapperProcessorConfig is the collector config for signozspanmapper.
|
||||
type spanMapperProcessorConfig struct {
|
||||
Groups []spanMapperProcessorGroup `yaml:"groups" json:"groups"`
|
||||
}
|
||||
|
||||
type spanMapperProcessorGroup struct {
|
||||
ID string `yaml:"id" json:"id"`
|
||||
ExistsAny spanMapperProcessorExistsAny `yaml:"exists_any" json:"exists_any"`
|
||||
Attributes []spanMapperProcessorAttribute `yaml:"attributes" json:"attributes"`
|
||||
}
|
||||
|
||||
type spanMapperProcessorExistsAny struct {
|
||||
Attributes []string `yaml:"attributes,omitempty" json:"attributes,omitempty"`
|
||||
Resource []string `yaml:"resource,omitempty" json:"resource,omitempty"`
|
||||
}
|
||||
|
||||
type spanMapperProcessorAttribute struct {
|
||||
Target string `yaml:"target" json:"target"`
|
||||
Context string `yaml:"context,omitempty" json:"context,omitempty"`
|
||||
Sources []spanMapperProcessorSource `yaml:"sources" json:"sources"`
|
||||
}
|
||||
|
||||
type spanMapperProcessorSource struct {
|
||||
Key string `yaml:"key" json:"key"`
|
||||
Action string `yaml:"action,omitempty" json:"action,omitempty"`
|
||||
}
|
||||
|
||||
func GenerateCollectorConfigWithSpanMapperProcessor(currentConfYaml []byte, groups []*SpanMapperGroupWithMappers) ([]byte, error) {
|
||||
var collectorConf map[string]any
|
||||
if err := yaml.Unmarshal(currentConfYaml, &collectorConf); err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, ErrCodeInvalidCollectorConfig, "failed to unmarshal collector config")
|
||||
}
|
||||
// rare but don't do anything in this case, also means it's just comments.
|
||||
if collectorConf == nil {
|
||||
collectorConf = map[string]any{}
|
||||
}
|
||||
|
||||
processors := map[string]any{}
|
||||
if existing, ok := collectorConf["processors"]; ok && existing != nil {
|
||||
p, ok := existing.(map[string]any)
|
||||
if !ok {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeInvalidCollectorConfig, "collector config 'processors' must be a mapping, got %T", existing)
|
||||
}
|
||||
processors = p
|
||||
}
|
||||
|
||||
procConfig := buildProcessorConfig(groups)
|
||||
|
||||
processors[ProcessorName] = procConfig
|
||||
collectorConf["processors"] = processors
|
||||
|
||||
out, err := yaml.Marshal(collectorConf)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, ErrCodeBuildMappingProcessorConfig, "failed to marshal collector config")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func buildProcessorConfig(groups []*SpanMapperGroupWithMappers) *spanMapperProcessorConfig {
|
||||
out := make([]spanMapperProcessorGroup, 0, len(groups))
|
||||
|
||||
for _, gm := range groups {
|
||||
rules := make([]spanMapperProcessorAttribute, 0, len(gm.Mappers))
|
||||
for _, m := range gm.Mappers {
|
||||
rules = append(rules, buildAttributeRule(m))
|
||||
}
|
||||
|
||||
out = append(out, spanMapperProcessorGroup{
|
||||
ID: gm.Group.Name,
|
||||
ExistsAny: spanMapperProcessorExistsAny{
|
||||
Attributes: gm.Group.Condition.Attributes,
|
||||
Resource: gm.Group.Condition.Resource,
|
||||
},
|
||||
Attributes: rules,
|
||||
})
|
||||
}
|
||||
|
||||
return &spanMapperProcessorConfig{Groups: out}
|
||||
}
|
||||
|
||||
// buildAttributeRule maps a single SpanMapper to a collector attribute rule.
|
||||
// Sources are sorted by Priority DESC (highest-priority first); read-from-
|
||||
// resource sources are encoded via the "resource." prefix on the key. Each
|
||||
// source carries its own action — "copy" is omitted to keep the emitted YAML
|
||||
// compact, and only "move" is set explicitly.
|
||||
func buildAttributeRule(m *SpanMapper) spanMapperProcessorAttribute {
|
||||
sources := make([]SpanMapperSource, len(m.Config.Sources))
|
||||
copy(sources, m.Config.Sources)
|
||||
sort.SliceStable(sources, func(i, j int) bool { return sources[i].Priority > sources[j].Priority })
|
||||
|
||||
out := make([]spanMapperProcessorSource, 0, len(sources))
|
||||
for _, s := range sources {
|
||||
key := s.Key
|
||||
if s.Context == FieldContextResource {
|
||||
key = FieldContextResource.StringValue() + "." + s.Key
|
||||
}
|
||||
var action string
|
||||
if s.Operation == SpanMapperOperationMove {
|
||||
action = SpanMapperOperationMove.StringValue()
|
||||
}
|
||||
out = append(out, spanMapperProcessorSource{Key: key, Action: action})
|
||||
}
|
||||
|
||||
ctx := FieldContextSpanAttribute
|
||||
if m.FieldContext == FieldContextResource {
|
||||
ctx = FieldContextResource
|
||||
}
|
||||
|
||||
return spanMapperProcessorAttribute{
|
||||
Target: m.Name,
|
||||
Context: ctx.StringValue(),
|
||||
Sources: out,
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
package spantypes
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestGenerateCollectorConfigWithSpanMapperProcessor(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
baseline := loadFixture(t, "collector_baseline.yaml")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
groups []*SpanMapperGroupWithMappers
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "no_groups",
|
||||
want: "collector_no_groups.yaml",
|
||||
},
|
||||
{
|
||||
name: "with_groups",
|
||||
groups: []*SpanMapperGroupWithMappers{
|
||||
{
|
||||
Group: newGroup("llm", []string{"model"}, []string{"service.name"}),
|
||||
Mappers: []*SpanMapper{
|
||||
newMapper("gen_ai.request.model", FieldContextResource,
|
||||
attrSrc("gen_ai.llm.model", SpanMapperOperationCopy, 3),
|
||||
attrSrc("llm.model", SpanMapperOperationCopy, 2),
|
||||
resSrc("service.name", SpanMapperOperationCopy, 1),
|
||||
),
|
||||
newMapper("gen_ai.request.tokens", FieldContextSpanAttribute,
|
||||
attrSrc("gen_ai.request_tokens", SpanMapperOperationCopy, 2),
|
||||
attrSrc("llm.tokens", SpanMapperOperationCopy, 1),
|
||||
),
|
||||
newMapper("gen_ai.request.input", FieldContextSpanAttribute,
|
||||
attrSrc("gen_ai.input", SpanMapperOperationMove, 2),
|
||||
attrSrc("llm.input", SpanMapperOperationMove, 1),
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
Group: newGroup("agent", []string{"agent."}, nil),
|
||||
Mappers: []*SpanMapper{
|
||||
newMapper("gen_ai.agent.name", FieldContextSpanAttribute,
|
||||
attrSrc("agent.name", SpanMapperOperationCopy, 2),
|
||||
attrSrc("llm.agent.name", SpanMapperOperationCopy, 1),
|
||||
),
|
||||
newMapper("gen_ai.agent.id", FieldContextSpanAttribute,
|
||||
attrSrc("gen_ai.agent.id", SpanMapperOperationCopy, 2),
|
||||
attrSrc("llm.agent.id", SpanMapperOperationCopy, 1),
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
Group: newGroup("tool", []string{"agent."}, nil),
|
||||
Mappers: []*SpanMapper{
|
||||
newMapper("gen_ai.tool.name", FieldContextSpanAttribute,
|
||||
attrSrc("ai.tool.name", SpanMapperOperationCopy, 2),
|
||||
attrSrc("llm.tool.name", SpanMapperOperationCopy, 1),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "collector_with_groups.yaml",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got, err := GenerateCollectorConfigWithSpanMapperProcessor(baseline, tc.groups)
|
||||
require.NoError(t, err)
|
||||
assertYAMLEqual(t, loadFixture(t, tc.want), got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateCollectorConfigWithSpanMapperProcessor_Errors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
in []byte
|
||||
}{
|
||||
{"processors_not_a_map", []byte("processors: not-a-map\n")},
|
||||
{"malformed_yaml", []byte(": :")},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := GenerateCollectorConfigWithSpanMapperProcessor(tc.in, nil)
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Ast(err, errors.TypeInvalidInput), "want TypeInvalidInput, got %v", err)
|
||||
assert.True(t, errors.Asc(err, ErrCodeInvalidCollectorConfig), "want ErrCodeInvalidCollectorConfig, got %v", err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAttributeRule(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mapper *SpanMapper
|
||||
want spanMapperProcessorAttribute
|
||||
}{
|
||||
{
|
||||
name: "priority_sort_and_resource_prefix",
|
||||
mapper: newMapper("gen_ai.request.model", FieldContextResource,
|
||||
attrSrc("llm.model", SpanMapperOperationCopy, 20),
|
||||
resSrc("service.name", SpanMapperOperationCopy, 10),
|
||||
attrSrc("gen_ai.llm.model", SpanMapperOperationCopy, 30),
|
||||
),
|
||||
want: spanMapperProcessorAttribute{
|
||||
Target: "gen_ai.request.model",
|
||||
Context: FieldContextResource.StringValue(),
|
||||
Sources: []spanMapperProcessorSource{
|
||||
{Key: "gen_ai.llm.model"},
|
||||
{Key: "llm.model"},
|
||||
{Key: "resource.service.name"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "per_source_actions",
|
||||
mapper: newMapper("gen_ai.request.input", FieldContextSpanAttribute,
|
||||
attrSrc("gen_ai.input", SpanMapperOperationMove, 20),
|
||||
attrSrc("llm.input", SpanMapperOperationCopy, 10),
|
||||
),
|
||||
want: spanMapperProcessorAttribute{
|
||||
Target: "gen_ai.request.input",
|
||||
Context: FieldContextSpanAttribute.StringValue(),
|
||||
Sources: []spanMapperProcessorSource{
|
||||
{Key: "gen_ai.input", Action: SpanMapperOperationMove.StringValue()},
|
||||
{Key: "llm.input"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, tc.want, buildAttributeRule(tc.mapper))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func loadFixture(t *testing.T, name string) []byte {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(filepath.Join("testdata", name))
|
||||
require.NoError(t, err)
|
||||
return b
|
||||
}
|
||||
|
||||
// assertYAMLEqual compares two YAML documents structurally so key order and
|
||||
// slice formatting do not matter.
|
||||
func assertYAMLEqual(t *testing.T, want, got []byte) {
|
||||
t.Helper()
|
||||
var w, g any
|
||||
require.NoError(t, yaml.Unmarshal(want, &w))
|
||||
require.NoError(t, yaml.Unmarshal(got, &g))
|
||||
assert.Equal(t, w, g)
|
||||
}
|
||||
|
||||
func newGroup(name string, attrs, res []string) *SpanMapperGroup {
|
||||
return &SpanMapperGroup{
|
||||
Name: name,
|
||||
Condition: SpanMapperGroupCondition{Attributes: attrs, Resource: res},
|
||||
Enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
func newMapper(name string, target FieldContext, sources ...SpanMapperSource) *SpanMapper {
|
||||
return &SpanMapper{
|
||||
Name: name,
|
||||
FieldContext: target,
|
||||
Config: SpanMapperConfig{Sources: sources},
|
||||
Enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
func attrSrc(key string, op SpanMapperOperation, priority int) SpanMapperSource {
|
||||
return SpanMapperSource{Key: key, Context: FieldContextSpanAttribute, Operation: op, Priority: priority}
|
||||
}
|
||||
|
||||
func resSrc(key string, op SpanMapperOperation, priority int) SpanMapperSource {
|
||||
return SpanMapperSource{Key: key, Context: FieldContextResource, Operation: op, Priority: priority}
|
||||
}
|
||||
@@ -21,9 +21,3 @@ type SpanMapperStore interface {
|
||||
UpdateMapper(ctx context.Context, mapper *SpanMapper) error
|
||||
DeleteMapper(ctx context.Context, orgID, groupID, id valuer.UUID) error
|
||||
}
|
||||
|
||||
// TraceStore defines the data access interface for trace detail queries.
|
||||
type TraceStore interface {
|
||||
GetTraceSummary(ctx context.Context, traceID string) (*TraceSummary, error)
|
||||
GetTraceSpans(ctx context.Context, traceID string, summary *TraceSummary) ([]StorableSpan, error)
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
processors:
|
||||
signozspanmapper:
|
||||
groups: []
|
||||
batch: {}
|
||||
exporters:
|
||||
otlp:
|
||||
endpoint: localhost:4317
|
||||
service:
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [otlp]
|
||||
processors: [signozspanmapper, batch]
|
||||
exporters: [otlp]
|
||||
@@ -1,17 +0,0 @@
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
processors:
|
||||
signozspanmapper:
|
||||
groups: []
|
||||
batch: {}
|
||||
exporters:
|
||||
otlp:
|
||||
endpoint: localhost:4317
|
||||
service:
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [otlp]
|
||||
processors: [signozspanmapper, batch]
|
||||
exporters: [otlp]
|
||||
@@ -1,67 +0,0 @@
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
processors:
|
||||
signozspanmapper:
|
||||
groups:
|
||||
- id: llm
|
||||
exists_any:
|
||||
attributes:
|
||||
- model
|
||||
resource:
|
||||
- service.name
|
||||
attributes:
|
||||
- target: gen_ai.request.model
|
||||
context: resource
|
||||
sources:
|
||||
- key: gen_ai.llm.model
|
||||
- key: llm.model
|
||||
- key: resource.service.name
|
||||
- target: gen_ai.request.tokens
|
||||
context: attribute
|
||||
sources:
|
||||
- key: gen_ai.request_tokens
|
||||
- key: llm.tokens
|
||||
- target: gen_ai.request.input
|
||||
context: attribute
|
||||
sources:
|
||||
- key: gen_ai.input
|
||||
action: move
|
||||
- key: llm.input
|
||||
action: move
|
||||
- id: agent
|
||||
exists_any:
|
||||
attributes:
|
||||
- agent.
|
||||
attributes:
|
||||
- target: gen_ai.agent.name
|
||||
context: attribute
|
||||
sources:
|
||||
- key: agent.name
|
||||
- key: llm.agent.name
|
||||
- target: gen_ai.agent.id
|
||||
context: attribute
|
||||
sources:
|
||||
- key: gen_ai.agent.id
|
||||
- key: llm.agent.id
|
||||
- id: tool
|
||||
exists_any:
|
||||
attributes:
|
||||
- agent.
|
||||
attributes:
|
||||
- target: gen_ai.tool.name
|
||||
context: attribute
|
||||
sources:
|
||||
- key: ai.tool.name
|
||||
- key: llm.tool.name
|
||||
batch: {}
|
||||
exporters:
|
||||
otlp:
|
||||
endpoint: localhost:4317
|
||||
service:
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [otlp]
|
||||
processors: [signozspanmapper, batch]
|
||||
exporters: [otlp]
|
||||
@@ -1,4 +1,4 @@
|
||||
package spantypes
|
||||
package tracedetailtypes
|
||||
|
||||
import (
|
||||
"slices"
|
||||
@@ -1,4 +1,4 @@
|
||||
package spantypes
|
||||
package tracedetailtypes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
9
pkg/types/tracedetailtypes/store.go
Normal file
9
pkg/types/tracedetailtypes/store.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package tracedetailtypes
|
||||
|
||||
import "context"
|
||||
|
||||
// TraceStore defines the data access interface for trace detail queries.
|
||||
type TraceStore interface {
|
||||
GetTraceSummary(ctx context.Context, traceID string) (*TraceSummary, error)
|
||||
GetTraceSpans(ctx context.Context, traceID string, summary *TraceSummary) ([]StorableSpan, error)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package spantypes
|
||||
package tracedetailtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package spantypes
|
||||
package tracedetailtypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -14,24 +14,6 @@ type Config struct {
|
||||
|
||||
// The directory from which to serve the web files.
|
||||
Directory string `mapstructure:"directory"`
|
||||
|
||||
// Settings that are exposed to the web.
|
||||
Settings Settings `mapstructure:"settings"`
|
||||
}
|
||||
|
||||
// Settings that are exposed to the web.
|
||||
type Settings struct {
|
||||
Posthog Posthog `mapstructure:"posthog"`
|
||||
|
||||
Appcues Appcues `mapstructure:"appcues"`
|
||||
}
|
||||
|
||||
type Posthog struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
}
|
||||
|
||||
type Appcues struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
}
|
||||
|
||||
func NewConfigFactory() factory.ConfigFactory {
|
||||
@@ -43,14 +25,6 @@ func newConfig() factory.Config {
|
||||
Enabled: true,
|
||||
Index: "index.html",
|
||||
Directory: "/etc/signoz/web",
|
||||
Settings: Settings{
|
||||
Posthog: Posthog{
|
||||
Enabled: true,
|
||||
},
|
||||
Appcues: Appcues{
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ func TestNewWithEnvProvider(t *testing.T) {
|
||||
Enabled: false,
|
||||
Index: def.Index,
|
||||
Directory: def.Directory,
|
||||
Settings: def.Settings,
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, actual)
|
||||
|
||||
@@ -2,8 +2,6 @@ package routerweb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -44,16 +42,8 @@ func New(ctx context.Context, settings factory.ProviderSettings, config web.Conf
|
||||
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "cannot read %q in web directory", config.Index)
|
||||
}
|
||||
|
||||
settingsJSON, err := json.Marshal(config.Settings)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "cannot marshal web settings to JSON")
|
||||
}
|
||||
|
||||
logger := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/web/routerweb").Logger()
|
||||
indexContents := web.NewIndex(ctx, logger, config.Index, raw, web.TemplateData{
|
||||
BaseHref: globalConfig.ExternalPathTrailing(),
|
||||
Settings: template.JS(settingsJSON),
|
||||
})
|
||||
indexContents := web.NewIndex(ctx, logger, config.Index, raw, web.TemplateData{BaseHref: globalConfig.ExternalPathTrailing()})
|
||||
|
||||
return &provider{
|
||||
config: config,
|
||||
|
||||
@@ -2,7 +2,6 @@ package routerweb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -20,11 +19,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func expectedHTML(baseHref string, settings web.Settings) string {
|
||||
settingsJSON, _ := json.Marshal(settings)
|
||||
return `<html><head><base href="` + baseHref + `" /></head><body><script>window.signozBootData={settings:` + string(settingsJSON) + `}</script>Welcome to test data!!!</body></html>`
|
||||
}
|
||||
|
||||
func startServer(t *testing.T, config web.Config, globalConfig global.Config) string {
|
||||
t.Helper()
|
||||
|
||||
@@ -60,79 +54,53 @@ func httpGet(t *testing.T, url string) string {
|
||||
func TestServeTemplatedIndex(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
emptySettings := web.Settings{}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
path string
|
||||
globalConfig global.Config
|
||||
webConfig web.Config
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "RootBaseHrefAtRoot",
|
||||
path: "/",
|
||||
globalConfig: global.Config{},
|
||||
webConfig: web.Config{Index: "valid_template.html", Directory: "testdata"},
|
||||
expected: expectedHTML("/", emptySettings),
|
||||
expected: `<html><head><base href="/" /></head><body>Welcome to test data!!!</body></html>`,
|
||||
},
|
||||
{
|
||||
name: "RootBaseHrefAtNonExistentPath",
|
||||
path: "/does-not-exist",
|
||||
globalConfig: global.Config{},
|
||||
webConfig: web.Config{Index: "valid_template.html", Directory: "testdata"},
|
||||
expected: expectedHTML("/", emptySettings),
|
||||
expected: `<html><head><base href="/" /></head><body>Welcome to test data!!!</body></html>`,
|
||||
},
|
||||
{
|
||||
name: "RootBaseHrefAtDirectory",
|
||||
path: "/assets",
|
||||
globalConfig: global.Config{},
|
||||
webConfig: web.Config{Index: "valid_template.html", Directory: "testdata"},
|
||||
expected: expectedHTML("/", emptySettings),
|
||||
expected: `<html><head><base href="/" /></head><body>Welcome to test data!!!</body></html>`,
|
||||
},
|
||||
{
|
||||
name: "SubPathBaseHrefAtRoot",
|
||||
path: "/",
|
||||
globalConfig: global.Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/signoz"}},
|
||||
webConfig: web.Config{Index: "valid_template.html", Directory: "testdata"},
|
||||
expected: expectedHTML("/signoz/", emptySettings),
|
||||
expected: `<html><head><base href="/signoz/" /></head><body>Welcome to test data!!!</body></html>`,
|
||||
},
|
||||
{
|
||||
name: "SubPathBaseHrefAtNonExistentPath",
|
||||
path: "/does-not-exist",
|
||||
globalConfig: global.Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/signoz"}},
|
||||
webConfig: web.Config{Index: "valid_template.html", Directory: "testdata"},
|
||||
expected: expectedHTML("/signoz/", emptySettings),
|
||||
expected: `<html><head><base href="/signoz/" /></head><body>Welcome to test data!!!</body></html>`,
|
||||
},
|
||||
{
|
||||
name: "SubPathBaseHrefAtDirectory",
|
||||
path: "/assets",
|
||||
globalConfig: global.Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/signoz"}},
|
||||
webConfig: web.Config{Index: "valid_template.html", Directory: "testdata"},
|
||||
expected: expectedHTML("/signoz/", emptySettings),
|
||||
},
|
||||
{
|
||||
name: "WithPopulatedSettings",
|
||||
path: "/",
|
||||
globalConfig: global.Config{},
|
||||
webConfig: web.Config{
|
||||
Index: "valid_template.html",
|
||||
Directory: "testdata",
|
||||
Settings: web.Settings{
|
||||
Posthog: web.Posthog{Enabled: true},
|
||||
Appcues: web.Appcues{Enabled: true},
|
||||
},
|
||||
},
|
||||
expected: expectedHTML("/", web.Settings{
|
||||
Posthog: web.Posthog{Enabled: true},
|
||||
Appcues: web.Appcues{Enabled: true},
|
||||
}),
|
||||
expected: `<html><head><base href="/signoz/" /></head><body>Welcome to test data!!!</body></html>`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
base := startServer(t, testCase.webConfig, testCase.globalConfig)
|
||||
base := startServer(t, web.Config{Index: "valid_template.html", Directory: "testdata"}, testCase.globalConfig)
|
||||
|
||||
assert.Equal(t, testCase.expected, strings.TrimSuffix(httpGet(t, base+testCase.path), "\n"))
|
||||
})
|
||||
|
||||
@@ -1 +1 @@
|
||||
<html><head><base href="[[.BaseHref]]" /></head><body><script>window.signozBootData={settings:[[.Settings]]}</script>Welcome to test data!!!</body></html>
|
||||
<html><head><base href="[[.BaseHref]]" /></head><body>Welcome to test data!!!</body></html>
|
||||
|
||||
@@ -11,14 +11,8 @@ import (
|
||||
|
||||
// Field names map to the HTML attributes they populate in the template:
|
||||
// - BaseHref → <base href="[[.BaseHref]]" />
|
||||
// - Settings → window.signozBootData = { settings: [[.Settings]] }
|
||||
type TemplateData struct {
|
||||
BaseHref string
|
||||
|
||||
// Settings is the pre-serialized JSON of web.Settings for injection into a
|
||||
// <script> block. The template.JS type prevents html/template from
|
||||
// HTML-escaping the value.
|
||||
Settings template.JS
|
||||
}
|
||||
|
||||
// If the template cannot be parsed or executed, the raw bytes are
|
||||
|
||||
Reference in New Issue
Block a user