@@ -6,22 +6,25 @@ import {
66} from './lib/env.ts'
77import { respond } from './lib/response.ts'
88import { log } from './lib/log.ts'
9- import { ARR , NUM , OBJ , STR , UNION } from './lib/validator.ts'
9+ import { ARR , NUM , OBJ , optional , STR , UNION } from './lib/validator.ts'
1010import { Asserted } from './lib/router.ts'
1111
1212const LogSchema = OBJ ( {
13- timestamp : STR ( ) ,
14- trace_id : STR ( ) ,
15- span_id : STR ( ) ,
16- severity_number : NUM ( ) ,
17- attributes : OBJ ( { } ) ,
18- event_name : STR ( ) ,
19- context : OBJ ( { } ) ,
20- } )
21-
22- const LogsInputSchema = UNION ( LogSchema , ARR ( LogSchema ) )
23-
24- export type Log = Asserted < typeof LogSchema >
13+ timestamp : NUM ( 'The timestamp of the log event' ) ,
14+ trace_id : NUM ( 'A float64 representation of the trace ID' ) ,
15+ span_id : NUM ( 'A float64 representation of the span ID' ) ,
16+ severity_number : NUM ( 'The severity number of the log event' ) ,
17+ attributes : optional ( OBJ ( { } , 'A map of attributes' ) ) ,
18+ event_name : STR ( 'The name of the event' ) ,
19+ service_version : optional ( STR ( 'Service version' ) ) ,
20+ service_instance_id : optional ( STR ( 'Service instance ID' ) ) ,
21+ } , 'A log event' )
22+ const LogsInputSchema = UNION (
23+ LogSchema ,
24+ ARR ( LogSchema , 'An array of log events' ) ,
25+ )
26+
27+ type Log = Asserted < typeof LogSchema >
2528type LogsInput = Asserted < typeof LogsInputSchema >
2629
2730const client = createClient ( {
@@ -34,26 +37,65 @@ const client = createClient({
3437 } ,
3538} )
3639
40+ export function float64ToId128 (
41+ { trace, span } : { trace : number ; span ?: number } ,
42+ ) {
43+ const id128 = new Uint8Array ( 16 )
44+ const view = new DataView ( id128 . buffer )
45+ view . setFloat64 ( 0 , trace , false )
46+ if ( span ) {
47+ view . setFloat64 ( 8 , span , false )
48+ } else {
49+ view . setFloat64 ( 8 , trace , false )
50+ }
51+ return id128
52+ }
53+
54+ export function bytesToHex ( bytes : Uint8Array ) {
55+ return Array . from ( bytes ) . map ( ( b ) => b . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' )
56+ }
57+ const escapeSql = ( s : unknown ) =>
58+ String ( s ?? '' ) . replace ( / \\ / g, '\\\\' ) . replace ( / ' / g, "''" )
59+
3760async function insertLogs (
38- resource : string ,
61+ service_name : string ,
3962 data : LogsInput ,
4063) {
4164 const logsToInsert = Array . isArray ( data ) ? data : [ data ]
42- if ( logsToInsert . length === 0 ) {
43- throw respond . NoContent ( )
44- }
45-
46- const values = logsToInsert . map ( ( log ) => ( {
47- ...log ,
48- resource,
49- } ) )
65+ if ( logsToInsert . length === 0 ) throw respond . NoContent ( )
66+
67+ const rows = logsToInsert . map ( ( log ) => {
68+ const traceHex = bytesToHex (
69+ float64ToId128 ( { trace : log . trace_id } ) ,
70+ )
71+ const spanHex = bytesToHex (
72+ float64ToId128 ( {
73+ trace : log . trace_id ,
74+ span : log . span_id ,
75+ } ) ,
76+ )
77+
78+ const ts = log . timestamp
79+ const attrs = escapeSql ( JSON . stringify ( log . attributes ) )
80+ const event = escapeSql ( log . event_name )
81+ const svc = escapeSql ( service_name )
82+ const svcVer = escapeSql ( log . service_version ?? 'unknown' )
83+ const svcInst = escapeSql ( log . service_instance_id ?? 'unknown' )
84+ // trace_id / span_id inserted as binary via unhex('<32hex>')
85+ return `('${ svc } ','${ svcVer } ','${ svcInst } ','${ ts } ',unhex('${ traceHex } '),unhex('${ spanHex } '),${ log . severity_number } ,'${ attrs } ','${ event } ')`
86+ } )
87+
88+ const q = `
89+ INSERT INTO logs
90+ (service_name, service_version, service_instance_id,
91+ timestamp, trace_id, span_id,
92+ severity_number, attributes, event_name)
93+ VALUES
94+ ${ rows . join ( ',' ) }
95+ ` . trim ( )
5096
5197 try {
52- await client . insert ( {
53- table : 'logs' ,
54- values,
55- format : 'JSONEachRow' ,
56- } )
98+ await client . command ( { query : q } )
5799 return respond . OK ( )
58100 } catch ( error ) {
59101 log . error ( 'Error inserting logs into ClickHouse:' , { error } )
@@ -89,9 +131,9 @@ async function getLogs({
89131 search ?: Record < string , string >
90132} ) {
91133 const queryParts : string [ ] = [ ]
92- const queryParams : Record < string , unknown > = { resource }
134+ const queryParams : Record < string , unknown > = { service_name : resource }
93135
94- queryParts . push ( 'resource = {resource :String}' )
136+ queryParts . push ( 'service_name = {service_name :String}' )
95137
96138 if ( level ) {
97139 queryParts . push ( 'severity_number = {level:UInt8}' )
@@ -134,11 +176,12 @@ async function getLogs({
134176 try {
135177 const resultSet = await client . query ( {
136178 query,
179+
137180 query_params : queryParams ,
138- format : 'JSONEachRow ' ,
181+ format : 'JSON ' ,
139182 } )
140183
141- return resultSet . json < Log [ ] > ( )
184+ return ( await resultSet . json < Log > ( ) ) . data
142185 } catch ( error ) {
143186 log . error ( 'Error querying logs from ClickHouse:' , { error } )
144187 throw respond . InternalServerError ( )
0 commit comments