1
+ const otelApi = require ( '@opentelemetry/api' )
2
+ const { OTLPMetricExporter } = require ( '@opentelemetry/exporter-metrics-otlp-proto' )
3
+ const { OTLPTraceExporter } = require ( '@opentelemetry/exporter-trace-otlp-proto' )
4
+ const { HttpInstrumentation } = require ( '@opentelemetry/instrumentation-http' )
5
+ const { IORedisInstrumentation } = require ( '@opentelemetry/instrumentation-ioredis' )
6
+ const { PgInstrumentation } = require ( '@opentelemetry/instrumentation-pg' )
7
+ const { PeriodicExportingMetricReader } = require ( '@opentelemetry/sdk-metrics' )
8
+ const otelSdk = require ( '@opentelemetry/sdk-node' )
9
+ const { get } = require ( 'lodash' )
10
+
11
+ const conf = require ( '@open-condo/config' )
12
+
13
+ const { getLogger } = require ( './logging' )
14
+
15
+ const DELIMETER = ':'
16
+ const KEYSTONE_MUTATION_QUERY_REGEX = / (?: m u t a t i o n | q u e r y ) \s + ( \w + ) /
17
+
18
+ const IS_OTEL_TRACING_ENABLED = conf . IS_OTEL_TRACING_ENABLED === '1'
19
+ const OTEL_CONFIG = conf . OTEL_CONFIG ? JSON . parse ( conf . OTEL_CONFIG ) : { }
20
+
21
+ const { tracesUrl, metricsUrl, headers = { } } = OTEL_CONFIG
22
+
23
+ const tracers = { }
24
+
25
+ if ( IS_OTEL_TRACING_ENABLED ) {
26
+ const sdk = new otelSdk . NodeSDK ( {
27
+ serviceName : 'condo' ,
28
+ traceExporter : new OTLPTraceExporter ( {
29
+ url : tracesUrl ,
30
+ headers : headers ,
31
+ } ) ,
32
+ metricReader : new PeriodicExportingMetricReader ( {
33
+ exporter : new OTLPMetricExporter ( {
34
+ url : metricsUrl ,
35
+ headers : headers ,
36
+ concurrencyLimit : 1 ,
37
+ } ) ,
38
+ } ) ,
39
+
40
+ instrumentations : [
41
+ new HttpInstrumentation ( ) ,
42
+ new PgInstrumentation ( ) ,
43
+ new IORedisInstrumentation ,
44
+ ] ,
45
+ } )
46
+
47
+ sdk . start ( )
48
+ }
49
+
50
+ const _getTracer = ( name ) => {
51
+ if ( ! tracers [ name ] ) {
52
+ tracers [ name ] = otelApi . trace . getTracer (
53
+ name ,
54
+ '1.0.0' ,
55
+ )
56
+ }
57
+ return tracers [ name ]
58
+ }
59
+
60
+ function _getTracedFunction ( { name, spanHook, tracer, ctx, f } ) {
61
+ return async function ( ...args ) {
62
+ // Sometimes you want the name of the trace to be calculated in runtime
63
+ const parsedName = typeof name === 'function' ? name ( ...args ) : name
64
+
65
+ return tracer . startActiveSpan ( parsedName , async ( span ) => {
66
+ spanHook ( span , ...args )
67
+
68
+ const res = await f . call ( ctx , ...args )
69
+ span . end ( )
70
+ return res
71
+ } )
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Monkey patch keystone with open telemetry tracing
77
+ */
78
+ class KeystoneTracingApp {
79
+
80
+ tracer = _getTracer ( '@open-condo/tracing/keystone-tracing-app' )
81
+
82
+ _getTracedAdapterFunction ( tracer , config , ctx , f ) {
83
+ const { name, listKey } = config
84
+
85
+ return _getTracedFunction ( {
86
+ name : name + DELIMETER + listKey ,
87
+ spanHook : ( span , _ ) => {
88
+ span . setAttribute ( 'type' , 'adapter' )
89
+ span . setAttribute ( 'listKey' , listKey )
90
+ span . setAttribute ( 'functionName' , name )
91
+ } ,
92
+ ctx, f, tracer,
93
+ } )
94
+ }
95
+
96
+ _patchKeystoneAdapter ( tracer , keystone ) {
97
+ for ( const listAdapter of Object . values ( get ( keystone , [ 'adapter' , 'listAdapters' ] ) ) ) {
98
+ const originalListAdapter = listAdapter
99
+ const listKey = listAdapter . key
100
+
101
+ listAdapter . find = this . _getTracedAdapterFunction ( tracer , { listKey, name : 'adapter:find' } , listAdapter , originalListAdapter . find )
102
+ listAdapter . findOne = this . _getTracedAdapterFunction ( tracer , { listKey, name : 'adapter:findOne' } , listAdapter , originalListAdapter . findOne )
103
+ listAdapter . findById = this . _getTracedAdapterFunction ( tracer , { listKey, name : 'adapter:findById' } , listAdapter , originalListAdapter . findById )
104
+ listAdapter . itemsQuery = this . _getTracedAdapterFunction ( tracer , { listKey, name : 'adapter:itemsQuery' } , listAdapter , originalListAdapter . itemsQuery )
105
+
106
+ listAdapter . create = this . _getTracedAdapterFunction ( tracer , { listKey, name : 'adapter:create' } , listAdapter , originalListAdapter . create )
107
+ listAdapter . delete = this . _getTracedAdapterFunction ( tracer , { listKey, name : 'adapter:delete' } , listAdapter , originalListAdapter . delete )
108
+ listAdapter . update = this . _getTracedAdapterFunction ( tracer , { listKey, name : 'adapter:update' } , listAdapter , originalListAdapter . update )
109
+
110
+ listAdapter . createMany = this . _getTracedAdapterFunction ( tracer , { listKey, name : 'adapter:createMany' } , listAdapter , originalListAdapter . createMany )
111
+ listAdapter . deleteMany = this . _getTracedAdapterFunction ( tracer , { listKey, name : 'adapter:createMany' } , listAdapter , originalListAdapter . deleteMany )
112
+ listAdapter . updateMany = this . _getTracedAdapterFunction ( tracer , { listKey, name : 'adapter:createMany' } , listAdapter , originalListAdapter . updateMany )
113
+ }
114
+ }
115
+
116
+ _patchKeystoneGraphQLExecutor ( tracer , keystone ) {
117
+ const originalExecuteGraphQL = keystone . executeGraphQL
118
+ keystone . executeGraphQL = ( { context, query, variables } ) => {
119
+ let queryName = undefined
120
+ if ( typeof query === 'string' ) {
121
+ const matches = query . match ( KEYSTONE_MUTATION_QUERY_REGEX )
122
+ if ( matches && matches [ 1 ] )
123
+ queryName = matches [ 1 ]
124
+ } else {
125
+ queryName = get ( query , [ 'definitions' , 0 , 'name' , 'value' ] )
126
+ }
127
+
128
+ return tracer . startActiveSpan ( 'gql' + DELIMETER + queryName , async ( span ) => {
129
+ span . setAttribute ( 'queryName' , queryName )
130
+
131
+ const result = originalExecuteGraphQL . call ( keystone , { context, query, variables } )
132
+ span . end ( )
133
+ return result
134
+ } )
135
+ }
136
+ }
137
+
138
+ async prepareMiddleware ( { keystone } ) {
139
+ if ( ! IS_OTEL_TRACING_ENABLED ) return
140
+
141
+ const tracer = this . tracer
142
+ this . _patchKeystoneGraphQLExecutor ( tracer , keystone )
143
+ this . _patchKeystoneAdapter ( tracer , keystone )
144
+ }
145
+ }
146
+
147
+
148
+ module . exports = {
149
+ KeystoneTracingApp,
150
+ }
0 commit comments