Skip to content

Commit 8c71b1b

Browse files
committed
fix: properly create/end otel spans
1 parent 1cdce1b commit 8c71b1b

3 files changed

Lines changed: 64 additions & 54 deletions

File tree

deno.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
"version": "1.9.6",
44

55
"tasks": {
6-
"test": "deno test --check --import-map=test/import_map.json --allow-read --allow-write --allow-ffi --allow-env=HOME,DENO_DIR,XDG_CACHE_HOME,DENO_SQLITE_PATH,DENO_SQLITE_LOCAL --allow-net test",
7-
"test:watch": "deno test --check --import-map=test/import_map.json --allow-read --allow-write --allow-ffi --allow-env=HOME,DENO_DIR,XDG_CACHE_HOME,DENO_SQLITE_PATH,DENO_SQLITE_LOCAL --allow-net --watch"
6+
"test": "deno test --check --import-map=test/import_map.json --allow-read --allow-write test",
7+
"test:watch": "deno test --check --import-map=test/import_map.json --allow-read --allow-write --watch"
88
},
99

1010
"lint": {

src/statement.ts

Lines changed: 21 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ abstract class StatementBase<DriverStatement, Params extends SchemaGeneric, Resu
8989
else throw new Error('A statement cannot be used until init() is called')
9090
}
9191

92-
protected encode_params = (params: Params | undefined): EncodedParams => {
93-
// SchemaGeneric is not assignable to an attribute, but I think we can trust the opentelemetry serialization. If not, we could switch over to the driver encoded params, which we know are definitely serialization friendly
94-
const span = telemetry.start_span('statement.encode_params', {params: params as any})
92+
@telemetry.instrument()
93+
protected encode_params(params: Params | undefined): EncodedParams {
94+
telemetry.attribute('params', params as any) // SchemaGeneric is not assignable to an attribute, but I think we can trust the opentelemetry serialization. If not, we could switch over to the driver encoded params, which we know are definitely serialization friendly
9595
const encoded_params: {[field: string]: any} = {}
9696
for (const field of Object.values(this.params)) {
9797
const val = params ? params[field.field_name] : undefined
@@ -102,18 +102,15 @@ abstract class StatementBase<DriverStatement, Params extends SchemaGeneric, Resu
102102
const message = e.format()._errors.join(',')
103103
throw new TypeError(`${message} on column ${field.table_name}.${field.field_name}:\n${Deno.inspect(params, { colors: true })}`)
104104
} else {
105-
span.recordException(e as Error)
106-
span.end()
107105
throw e
108106
}
109107
}
110108
}
111-
span.end()
112109
return encoded_params
113110
}
114111

115-
protected decode_result = (result: RawRowData): Result => {
116-
const span = telemetry.start_span('statement.decode_result')
112+
@telemetry.instrument()
113+
protected decode_result(result: RawRowData): Result {
117114
const decoded_result: {[field: string]: any} = {}
118115
for (const [field_name, val] of Object.entries(result)) {
119116
const field = this.get_result_field(field_name)
@@ -126,8 +123,6 @@ abstract class StatementBase<DriverStatement, Params extends SchemaGeneric, Resu
126123
// decoded_result[field_name] = val
127124
// }
128125
} catch (e) {
129-
span.recordException(e as Error)
130-
span.end()
131126
if (e instanceof z.ZodError) {
132127
const message = e.format()._errors.join(',')
133128
throw new TypeError(`${message} on column ${field.table_name}.${field.field_name}:\n${Deno.inspect(result, { colors: true })}`)
@@ -137,67 +132,45 @@ abstract class StatementBase<DriverStatement, Params extends SchemaGeneric, Resu
137132
}
138133
}
139134
// I dont know how to convert these partial types into full types, so we just cast here
140-
span.end()
141135
return decoded_result as Result
142136
}
143137

144-
public one = (...params: OptionalOnEmpty<Params>): Result | undefined => {
145-
const span = telemetry.start_span('statement.one', {sql: this.sql})
146-
try {
147-
return this.one_impl(...params)
148-
} catch (e) {
149-
span.recordException(e as Error)
150-
throw e
151-
} finally {
152-
span.end()
153-
}
138+
@telemetry.instrument()
139+
public one(...params: OptionalOnEmpty<Params>): Result | undefined {
140+
telemetry.attribute('sql', this.sql)
141+
return this.one_impl(...params)
154142
}
155143
protected abstract one_impl(...params: OptionalOnEmpty<Params>): Result | undefined
156144

157-
public all = (...params: OptionalOnEmpty<Params>): Result[] => {
158-
const span = telemetry.start_span('statement.all', {sql: this.sql})
159-
span.setAttribute('params', params as any) // SchemaGeneric is not assignable to an attribute, but I think we can trust the opentelemetry serialization. If not, we could switch over to the driver encoded params, which we know are definitely serialization friendly
160-
try {
161-
return this.all_impl(...params)
162-
} catch (e) {
163-
span.recordException(e as Error)
164-
throw e
165-
} finally {
166-
span.end()
167-
}
145+
@telemetry.instrument()
146+
public all(...params: OptionalOnEmpty<Params>): Result[] {
147+
telemetry.attribute('sql', this.sql)
148+
return this.all_impl(...params)
168149
}
169150
protected abstract all_impl(...params: OptionalOnEmpty<Params>): Result[]
170151

171-
public exec = (...params: OptionalOnEmpty<Params>): ExecInfo => {
172-
const span = telemetry.start_span('statement.exec', {sql: this.sql})
173-
span.setAttribute('params', params as any) // SchemaGeneric is not assignable to an attribute, but I think we can trust the opentelemetry serialization. If not, we could switch over to the driver encoded params, which we know are definitely serialization friendly
174-
try {
175-
return this.exec_impl(...params)
176-
} catch (e) {
177-
span.recordException(e as Error)
178-
throw e
179-
} finally {
180-
span.end()
181-
}
152+
@telemetry.instrument()
153+
public exec(...params: OptionalOnEmpty<Params>): ExecInfo {
154+
telemetry.attribute('sql', this.sql)
155+
telemetry.attribute('params', params as any) // SchemaGeneric is not assignable to an attribute, but I think we can trust the opentelemetry serialization. If not, we could switch over to the driver encoded params, which we know are definitely serialization friendly
156+
return this.exec_impl(...params)
182157
}
183158
protected abstract exec_impl(...params: OptionalOnEmpty<Params>): ExecInfo
184159

185-
public prepare = (driver: Driver): void => {
186-
const span = telemetry.start_span('statement.exec', {sql: this.sql})
160+
@telemetry.instrument()
161+
public prepare(driver: Driver): void {
162+
telemetry.attribute('sql', this.sql)
187163
this._driver = driver
188164
try {
189165
this._stmt = this.prepare_impl(this.sql)
190166
} catch (e) {
191-
span.recordException(e as Error)
192167
if (e instanceof Error === false) throw e
193168
throw new Error(`${e.message}
194169
${'```'}
195170
${this.sql}
196171
${'```'}`, {
197172
cause: e
198173
})
199-
} finally {
200-
span.end()
201174
}
202175
}
203176

src/telemetry.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,46 @@ import * as otel from '@opentelemetry/api'
22

33
const TRACE_NAME = '@torm/sqlite'
44

5-
const trace = otel.trace.getTracer(TRACE_NAME)
5+
const tracer = otel.trace.getTracer(TRACE_NAME)
66

7-
export function start_span(name: string, attributes?: otel.Attributes) {
8-
const span = trace.startSpan(name, {attributes})
9-
return span
7+
export function attribute(name: string, value: any) {
8+
otel.trace.getActiveSpan()?.setAttribute(name, value)
9+
}
10+
11+
export function instrument(span_name?: string) {
12+
return function(original_method: any, context: ClassMethodDecoratorContext) {
13+
const method_name = String(context.name);
14+
15+
function replacement_method(...args: any[]) {
16+
// @ts-ignore
17+
const thisClass = this as any
18+
const final_span_name = span_name || `${thisClass.constructor.name}.${method_name}`;
19+
20+
return tracer.startActiveSpan(final_span_name, (span: otel.Span) => {
21+
try {
22+
span.setAttributes({
23+
'class.name': thisClass.constructor.name,
24+
'method.name': method_name,
25+
});
26+
27+
// execute the original method
28+
// note that currentl we only support synchronous methods. In the future, we can easily add promise support here, but since our only driver is sqlite, this is fine for now
29+
return original_method.call(thisClass, ...args);
30+
} catch (error) {
31+
span.recordException(error as Error);
32+
throw error;
33+
} finally {
34+
span.end()
35+
}
36+
});
37+
}
38+
39+
context.addInitializer(function (this) {
40+
const thisClass = this as any
41+
// also bind any method we set up this way
42+
thisClass[method_name] = thisClass[method_name].bind(thisClass)
43+
});
44+
45+
return replacement_method;
46+
};
1047
}

0 commit comments

Comments
 (0)