1
1
import { TSource } from '@hyperdx/common-utils/dist/types' ;
2
2
3
- function getDefaults ( jsonColumns : string [ ] = [ ] ) {
4
- const spanAttributeField = 'SpanAttributes' ;
5
- const isJsonColumn = jsonColumns . includes ( spanAttributeField ) ;
3
+ const COALESCE_FIELDS_LIMIT = 100 ;
6
4
7
- const formatFieldAccess = ( field : string , key : string ) => {
8
- if ( isJsonColumn ) {
9
- return `${ field } .\`${ key } \`` ;
5
+ // Helper function to format field access based on column type
6
+ function formatFieldAccess (
7
+ field : string ,
8
+ key : string ,
9
+ isJsonColumn : boolean ,
10
+ ) : string {
11
+ return isJsonColumn ? `${ field } .\`${ key } \`` : `${ field } ['${ key } ']` ;
12
+ }
13
+
14
+ /**
15
+ * Creates a 'coalesced' SQL query that checks whether each given field exists
16
+ * and returns the first non-empty value.
17
+ *
18
+ * The list of fields should be ordered from highest precedence to lowest.
19
+ *
20
+ * @param fields list of fields (in order) to coalesce
21
+ * @param isJSONColumn whether the fields are JSON columns
22
+ * @returns a SQL query string that coalesces the fields
23
+ */
24
+ export function makeCoalescedFieldsAccessQuery (
25
+ fields : string [ ] ,
26
+ isJSONColumn : boolean ,
27
+ ) : string {
28
+ if ( fields . length === 0 ) {
29
+ throw new Error (
30
+ 'Empty fields array passed while trying to build a coalesced field access query' ,
31
+ ) ;
32
+ }
33
+
34
+ if ( fields . length > COALESCE_FIELDS_LIMIT ) {
35
+ throw new Error (
36
+ `Too many fields (${ fields . length } ) passed while trying to build a coalesced field access query. Maximum allowed is ${ COALESCE_FIELDS_LIMIT } ` ,
37
+ ) ;
38
+ }
39
+
40
+ if ( fields . length === 1 ) {
41
+ if ( isJSONColumn ) {
42
+ return `if(toString(${ fields [ 0 ] } ) != '', toString(${ fields [ 0 ] } ), '')` ;
10
43
} else {
11
- return `${ field } [' ${ key } '] ` ;
44
+ return `nullif( ${ fields [ 0 ] } , '') ` ;
12
45
}
13
- } ;
46
+ }
14
47
15
- let dbStatement = `coalesce(nullif(${ formatFieldAccess ( spanAttributeField , 'db.query.text' ) } , ''), nullif(${ formatFieldAccess ( spanAttributeField , 'db.statement' ) } , ''))` ;
16
-
17
- // ClickHouse does not support NULLIF(some_dynamic_column)
18
- // so we instead use toString() and an empty string check to check for
19
- // existence of the serverAddress/httpHost to build the span name
20
- if ( isJsonColumn ) {
21
- dbStatement = `
22
- coalesce(
23
- if(
24
- toString(${ formatFieldAccess ( spanAttributeField , 'db.query.text' ) } ) != '',
25
- toString(${ formatFieldAccess ( spanAttributeField , 'db.query.text' ) } ),
26
- if(
27
- toString(${ formatFieldAccess ( spanAttributeField , 'db.statement' ) } ) != '',
28
- toString(${ formatFieldAccess ( spanAttributeField , 'db.statement' ) } ),
29
- ''
30
- )
31
- )
32
- )
33
- ` ;
48
+ if ( isJSONColumn ) {
49
+ // For JSON columns, build nested if statements
50
+ let query = '' ;
51
+ for ( let i = 0 ; i < fields . length ; i ++ ) {
52
+ const field = fields [ i ] ;
53
+ const isLast = i === fields . length - 1 ;
54
+
55
+ query += `if(
56
+ toString(${ field } ) != '',
57
+ toString(${ field } ),` ;
58
+
59
+ if ( isLast ) {
60
+ query += `''\n` ;
61
+ } else {
62
+ query += '\n' ;
63
+ }
64
+ }
65
+
66
+ // Close all the if statements
67
+ for ( let i = 0 ; i < fields . length ; i ++ ) {
68
+ query += ')' ;
69
+ }
70
+
71
+ return `coalesce(\n${ query } \n)` ;
72
+ } else {
73
+ // For non-JSON columns, use nullif with coalesce
74
+ const nullifExpressions = fields . map ( field => `nullif(${ field } , '')` ) ;
75
+ return `coalesce(${ nullifExpressions . join ( ', ' ) } )` ;
34
76
}
77
+ }
78
+
79
+ function getDefaults ( {
80
+ spanAttributeField = 'SpanAttributes' ,
81
+ isAttributeFieldJSON = false ,
82
+ } : {
83
+ spanAttributeField ?: string ;
84
+ isAttributeFieldJSON ?: boolean ;
85
+ } = { } ) {
86
+ const dbStatement = makeCoalescedFieldsAccessQuery (
87
+ [
88
+ formatFieldAccess (
89
+ spanAttributeField ,
90
+ 'db.query.text' ,
91
+ isAttributeFieldJSON ,
92
+ ) ,
93
+ formatFieldAccess (
94
+ spanAttributeField ,
95
+ 'db.statement' ,
96
+ isAttributeFieldJSON ,
97
+ ) ,
98
+ ] ,
99
+ isAttributeFieldJSON ,
100
+ ) ;
35
101
36
102
return {
37
103
duration : 'Duration' ,
@@ -41,17 +107,40 @@ function getDefaults(jsonColumns: string[] = []) {
41
107
spanName : 'SpanName' ,
42
108
spanKind : 'SpanKind' ,
43
109
severityText : 'StatusCode' ,
44
- k8sResourceName : formatFieldAccess ( spanAttributeField , 'k8s.resource.name' ) ,
45
- k8sPodName : formatFieldAccess ( spanAttributeField , 'k8s.pod.name' ) ,
46
- httpScheme : formatFieldAccess ( spanAttributeField , 'http.scheme' ) ,
47
- serverAddress : formatFieldAccess ( spanAttributeField , 'server.address' ) ,
48
- httpHost : formatFieldAccess ( spanAttributeField , 'http.host' ) ,
110
+ k8sResourceName : formatFieldAccess (
111
+ spanAttributeField ,
112
+ 'k8s.resource.name' ,
113
+ isAttributeFieldJSON ,
114
+ ) ,
115
+ k8sPodName : formatFieldAccess (
116
+ spanAttributeField ,
117
+ 'k8s.pod.name' ,
118
+ isAttributeFieldJSON ,
119
+ ) ,
120
+ httpScheme : formatFieldAccess (
121
+ spanAttributeField ,
122
+ 'http.scheme' ,
123
+ isAttributeFieldJSON ,
124
+ ) ,
125
+ serverAddress : formatFieldAccess (
126
+ spanAttributeField ,
127
+ 'server.address' ,
128
+ isAttributeFieldJSON ,
129
+ ) ,
130
+ httpHost : formatFieldAccess (
131
+ spanAttributeField ,
132
+ 'http.host' ,
133
+ isAttributeFieldJSON ,
134
+ ) ,
49
135
dbStatement,
50
136
} ;
51
137
}
52
138
53
139
export function getExpressions ( source ?: TSource , jsonColumns : string [ ] = [ ] ) {
54
- const defaults = getDefaults ( jsonColumns ) ;
140
+ const spanAttributeField =
141
+ source ?. eventAttributesExpression || 'SpanAttributes' ;
142
+ const isAttributeFieldJSON = jsonColumns . includes ( spanAttributeField ) ;
143
+ const defaults = getDefaults ( { spanAttributeField, isAttributeFieldJSON } ) ;
55
144
56
145
const fieldExpressions = {
57
146
// General
0 commit comments