66 type AsyncRemoteCallback ,
77 drizzle as drizzleProxy ,
88} from "drizzle-orm/sqlite-proxy" ;
9+ import { withSpan } from "../telemetry" ;
910import { resolveRuntimeDatabaseConfig } from "./config" ;
1011import * as schema from "./schema" ;
1112
@@ -14,6 +15,11 @@ dotenv.config();
1415type QueryMethod = Parameters < AsyncRemoteCallback > [ 2 ] ;
1516type BatchQuery = Parameters < AsyncBatchRemoteCallback > [ 0 ] [ number ] ;
1617type ProxyQueryResult = Awaited < ReturnType < AsyncRemoteCallback > > ;
18+ type LibsqlClient = ReturnType < typeof createClient > ;
19+ type InstrumentableLibsqlClient = {
20+ execute : ( ...args : unknown [ ] ) => Promise < unknown > ;
21+ batch : ( ...args : unknown [ ] ) => Promise < unknown > ;
22+ } ;
1723
1824const databaseConfig = resolveRuntimeDatabaseConfig ( ) ;
1925
@@ -25,6 +31,16 @@ async function executeProxyQuery(
2531 sql : string ,
2632 params : unknown [ ] ,
2733 method : QueryMethod ,
34+ ) : Promise < ProxyQueryResult > {
35+ return withDbSpan ( sql , method , ( ) =>
36+ executeProxyQueryUntraced ( sql , params , method ) ,
37+ ) ;
38+ }
39+
40+ async function executeProxyQueryUntraced (
41+ sql : string ,
42+ params : unknown [ ] ,
43+ method : QueryMethod ,
2844) : Promise < ProxyQueryResult > {
2945 const proxyUrl =
3046 databaseConfig . mode === "proxy" ? databaseConfig . proxyUrl : undefined ;
@@ -51,6 +67,15 @@ async function executeProxyQuery(
5167
5268async function executeProxyBatch (
5369 queries : BatchQuery [ ] ,
70+ ) : Promise < ProxyQueryResult [ ] > {
71+ return withDbBatchSpan (
72+ queries . map ( ( query ) => query . sql ) ,
73+ ( ) => executeProxyBatchUntraced ( queries ) ,
74+ ) ;
75+ }
76+
77+ async function executeProxyBatchUntraced (
78+ queries : BatchQuery [ ] ,
5479) : Promise < ProxyQueryResult [ ] > {
5580 const proxyUrl =
5681 databaseConfig . mode === "proxy" ? databaseConfig . proxyUrl : undefined ;
@@ -84,6 +109,149 @@ function ensureTrailingSlash(url: string) {
84109 return url . endsWith ( "/" ) ? url : `${ url } /` ;
85110}
86111
112+ function instrumentLibsqlClient ( client : LibsqlClient ) : LibsqlClient {
113+ const instrumented = client as unknown as InstrumentableLibsqlClient ;
114+ const execute = instrumented . execute . bind ( client ) ;
115+ const batch = instrumented . batch . bind ( client ) ;
116+
117+ instrumented . execute = ( ...args : unknown [ ] ) => {
118+ const sql = sqlFromStatement ( args [ 0 ] ) ;
119+ return withDbSpan ( sql , "execute" , ( ) => execute ( ...args ) ) ;
120+ } ;
121+
122+ instrumented . batch = ( ...args : unknown [ ] ) => {
123+ const statements = Array . isArray ( args [ 0 ] ) ? args [ 0 ] : [ ] ;
124+
125+ return withDbBatchSpan (
126+ statements . map ( ( statement ) => sqlFromStatement ( statement ) ) ,
127+ ( ) => batch ( ...args ) ,
128+ ) ;
129+ } ;
130+
131+ return client ;
132+ }
133+
134+ function withDbSpan < T > (
135+ sql : string | undefined ,
136+ method : string ,
137+ fn : ( ) => Promise < T > ,
138+ ) {
139+ const operation = sqlOperation ( sql ) ?? method ;
140+ const collection = sqlCollection ( sql ) ;
141+
142+ return withSpan (
143+ "db.query" ,
144+ dbQueryAttributes ( operation , collection , method ) ,
145+ async ( span ) => {
146+ if ( await shouldInjectDbLatencySpike ( operation , collection ) ) {
147+ span ?. setAttribute ( "app.result" , "latency_spike" ) ;
148+ await sleep ( 5000 ) ;
149+ }
150+
151+ return fn ( ) ;
152+ } ,
153+ ) ;
154+ }
155+
156+ function withDbBatchSpan < T > (
157+ sqlStatements : ( string | undefined ) [ ] ,
158+ fn : ( ) => Promise < T > ,
159+ ) {
160+ return withSpan ( "db.batch" , dbBatchAttributes ( sqlStatements ) , async ( ) =>
161+ fn ( ) ,
162+ ) ;
163+ }
164+
165+ function dbQueryAttributes (
166+ operation : string ,
167+ collection : string | undefined ,
168+ method : string ,
169+ ) {
170+ return {
171+ "db.system" : "sqlite" ,
172+ "db.operation" : operation ,
173+ "db.query.method" : method ,
174+ ...( collection ? { "db.collection" : collection } : { } ) ,
175+ } ;
176+ }
177+
178+ function dbBatchAttributes ( sqlStatements : ( string | undefined ) [ ] ) {
179+ const operations = [
180+ ...new Set ( sqlStatements . map ( sqlOperation ) . filter ( isString ) ) ,
181+ ] ;
182+ const collections = [
183+ ...new Set ( sqlStatements . map ( sqlCollection ) . filter ( isString ) ) ,
184+ ] ;
185+
186+ return {
187+ "db.system" : "sqlite" ,
188+ "db.operation" : operations . length === 1 ? operations [ 0 ] : "batch" ,
189+ "db.statement_count" : sqlStatements . length ,
190+ ...( collections . length === 1 ? { "db.collection" : collections [ 0 ] } : { } ) ,
191+ } ;
192+ }
193+
194+ async function shouldInjectDbLatencySpike (
195+ operation : string ,
196+ collection : string | undefined ,
197+ ) {
198+ return (
199+ operation === "select" &&
200+ collection === "team_members" &&
201+ ( await isRequestFaultActive ( "api-team-db-latency-spike" ) )
202+ ) ;
203+ }
204+
205+ async function isRequestFaultActive ( faultName : string ) {
206+ try {
207+ const { headers } = await import ( "next/headers" ) ;
208+ return ( await headers ( ) ) . get ( "x-faults" ) ?. trim ( ) === faultName ;
209+ } catch {
210+ return false ;
211+ }
212+ }
213+
214+ function sleep ( ms : number ) {
215+ return new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
216+ }
217+
218+ function sqlFromStatement ( statement : unknown ) : string | undefined {
219+ if ( typeof statement === "string" ) {
220+ return statement ;
221+ }
222+ if ( Array . isArray ( statement ) && typeof statement [ 0 ] === "string" ) {
223+ return statement [ 0 ] ;
224+ }
225+ if ( isRecord ( statement ) && typeof statement . sql === "string" ) {
226+ return statement . sql ;
227+ }
228+
229+ return undefined ;
230+ }
231+
232+ function sqlOperation ( sql : string | undefined ) {
233+ return sql ?. trim ( ) . split ( / \s + / , 1 ) [ 0 ] ?. toLowerCase ( ) ;
234+ }
235+
236+ function sqlCollection ( sql : string | undefined ) {
237+ if ( ! sql ) return undefined ;
238+
239+ const normalized = sql . replace ( / [ " ` ] / g, " " ) ;
240+ const match = normalized . match (
241+ / \b (?: f r o m | i n t o | u p d a t e | j o i n ) \s + ( [ a - z A - Z _ ] [ a - z A - Z 0 - 9 _ ] * ) / i,
242+ ) ;
243+
244+ return match ?. [ 1 ] ?. toLowerCase ( ) ;
245+ }
246+
247+ function isRecord ( value : unknown ) : value is Record < string , unknown > {
248+ return typeof value === "object" && value !== null ;
249+ }
250+
251+ function isString ( value : string | undefined ) : value is string {
252+ return typeof value === "string" ;
253+ }
254+
87255function createDatabase ( ) {
88256 if ( databaseConfig . mode === "proxy" ) {
89257 return drizzleProxy ( executeProxyQuery , executeProxyBatch , { schema } ) ;
@@ -100,7 +268,7 @@ function createDatabase() {
100268 } ,
101269 ) ;
102270
103- return drizzleLibsql ( client , { schema } ) ;
271+ return drizzleLibsql ( instrumentLibsqlClient ( client ) , { schema } ) ;
104272}
105273
106274export const db = createDatabase ( ) ;
0 commit comments