@@ -29,7 +29,8 @@ export interface OtlpExportCounts {
2929// OTLP proto SpanKind reserves 0 for UNSPECIFIED, so api values shift by 1
3030// on the wire. AGGREGATION_TEMPORALITY has no enum in @opentelemetry /api.
3131const otlpSpanKind = ( kind : SpanKind ) : number => kind + 1 ;
32- const AGGREGATION_TEMPORALITY_DELTA = 1 ;
32+ const AGGREGATION_TEMPORALITY_CUMULATIVE = 2 ;
33+ const OTLP_SCHEMA_URL = "https://opentelemetry.io/schemas/1.24.0" ;
3334const zipAsync = promisify ( zip ) ;
3435
3536/**
@@ -60,6 +61,14 @@ export async function writeOtlpZipExport(
6061 } ) ;
6162}
6263
64+ /** Per-export state threaded through the metric mapper for cumulative sums. */
65+ interface ExportState {
66+ readonly cumulative : {
67+ startTimeUnixNano : string | undefined ;
68+ readonly totals : Map < string , bigint > ;
69+ } ;
70+ }
71+
6372// Per-signal layout driving file names, envelope JSON keys, routing, and counts.
6473const ENVELOPES = [
6574 {
@@ -69,7 +78,7 @@ const ENVELOPES = [
6978 resourceKey : "resourceLogs" ,
7079 scopeKey : "scopeLogs" ,
7180 recordsKey : "logRecords" ,
72- toRecords : ( e : TelemetryEvent ) => [ toOtlpLogRecord ( e ) ] ,
81+ toRecords : ( e : TelemetryEvent , _state : ExportState ) => [ toOtlpLogRecord ( e ) ] ,
7382 } ,
7483 {
7584 signal : "trace" ,
@@ -78,7 +87,7 @@ const ENVELOPES = [
7887 resourceKey : "resourceSpans" ,
7988 scopeKey : "scopeSpans" ,
8089 recordsKey : "spans" ,
81- toRecords : ( e : TelemetryEvent ) => [ toOtlpSpan ( e ) ] ,
90+ toRecords : ( e : TelemetryEvent , _state : ExportState ) => [ toOtlpSpan ( e ) ] ,
8291 } ,
8392 {
8493 signal : "metric" ,
@@ -96,7 +105,7 @@ const ENVELOPES = [
96105 resourceKey : string ;
97106 scopeKey : string ;
98107 recordsKey : string ;
99- toRecords : ( event : TelemetryEvent ) => readonly unknown [ ] ;
108+ toRecords : ( event : TelemetryEvent , state : ExportState ) => readonly unknown [ ] ;
100109} > ;
101110
102111async function writeOtlpJsonFiles (
@@ -105,17 +114,20 @@ async function writeOtlpJsonFiles(
105114 context : TelemetryContext ,
106115) : Promise < OtlpExportCounts > {
107116 const resource = JSON . stringify ( toOtlpResource ( context ) ) ;
108- const scope = JSON . stringify ( toOtlpScope ( ) ) ;
117+ const scope = JSON . stringify ( toOtlpScope ( context . extensionVersion ) ) ;
109118 const entries = await Promise . all (
110119 ENVELOPES . map ( async ( envelope ) => ( {
111120 ...envelope ,
112121 writer : await openEnvelope (
113122 path . join ( dir , envelope . file ) ,
114- `{"${ envelope . resourceKey } ":[{"resource":${ resource } ,"${ envelope . scopeKey } ":[{"scope":${ scope } ,"${ envelope . recordsKey } ":[` ,
123+ `{"${ envelope . resourceKey } ":[{"resource":${ resource } ,"schemaUrl":" ${ OTLP_SCHEMA_URL } "," ${ envelope . scopeKey } ":[{"scope":${ scope } ,"schemaUrl":" ${ OTLP_SCHEMA_URL } " ,"${ envelope . recordsKey } ":[` ,
115124 "]}]}]}\n" ,
116125 ) ,
117126 } ) ) ,
118127 ) ;
128+ const state : ExportState = {
129+ cumulative : { startTimeUnixNano : undefined , totals : new Map ( ) } ,
130+ } ;
119131 const counts : Record < keyof OtlpExportCounts , number > = {
120132 logs : 0 ,
121133 traces : 0 ,
@@ -130,7 +142,7 @@ async function writeOtlpJsonFiles(
130142 continue ;
131143 }
132144 counts [ entry . counter ] += 1 ;
133- for ( const record of entry . toRecords ( event ) ) {
145+ for ( const record of entry . toRecords ( event , state ) ) {
134146 await entry . writer . write ( record ) ;
135147 }
136148 }
@@ -203,11 +215,11 @@ function toOtlpResource(context: TelemetryContext): JsonObject {
203215 attributes : keyValues ( {
204216 "service.name" : "coder-vscode-extension" ,
205217 "service.version" : context . extensionVersion ,
206- "coder.machine.id" : context . machineId ,
207- "coder.session.id" : context . sessionId ,
218+ "service.instance.id" : context . sessionId ,
219+ "host.id" : context . machineId ,
220+ "host.arch" : context . hostArch ,
208221 "os.type" : context . osType ,
209222 "os.version" : context . osVersion ,
210- "host.arch" : context . hostArch ,
211223 "vscode.platform.name" : context . platformName ,
212224 "vscode.platform.version" : context . platformVersion ,
213225 "coder.deployment.url" : context . deploymentUrl ,
@@ -216,8 +228,8 @@ function toOtlpResource(context: TelemetryContext): JsonObject {
216228}
217229
218230/** OTLP `InstrumentationScope` shared by every record. */
219- function toOtlpScope ( ) : JsonObject {
220- return { name : "coder.vscode-coder.telemetry.export" } ;
231+ function toOtlpScope ( version : string ) : JsonObject {
232+ return { name : "coder.vscode-coder.telemetry.export" , version } ;
221233}
222234
223235function toOtlpLogRecord ( event : TelemetryEvent ) : JsonObject {
@@ -285,11 +297,14 @@ function spanStatus(event: TelemetryEvent): JsonObject {
285297}
286298
287299/** A metric event yields one record per measurement (http.requests fans out). */
288- function toOtlpMetricRecords ( event : TelemetryEvent ) : JsonObject [ ] {
300+ function toOtlpMetricRecords (
301+ event : TelemetryEvent ,
302+ state : ExportState ,
303+ ) : JsonObject [ ] {
289304 const timeUnixNano = toUnixNano ( event . timestamp ) ;
290305 const attributes = metricAttributes ( event ) ;
291306 if ( event . eventName === "http.requests" ) {
292- return toHttpRequestMetrics ( event , timeUnixNano , attributes ) ;
307+ return toHttpRequestMetrics ( event , timeUnixNano , attributes , state ) ;
293308 }
294309 return toGaugeMetrics (
295310 event ,
@@ -303,6 +318,7 @@ function toHttpRequestMetrics(
303318 event : TelemetryEvent ,
304319 timeUnixNano : string ,
305320 attributes : JsonObject [ ] ,
321+ state : ExportState ,
306322) : JsonObject [ ] {
307323 const counts : Array < [ string , number ] > = [ ] ;
308324 const gauges : Array < [ string , number ] > = [ ] ;
@@ -316,38 +332,64 @@ function toHttpRequestMetrics(
316332 gauges . push ( [ name , value ] ) ;
317333 }
318334 }
319- const startTimeUnixNano = String (
335+ const windowStartUnixNano = String (
320336 BigInt ( timeUnixNano ) - nanosFromSeconds ( windowSeconds ?? 0 ) ,
321337 ) ;
322-
323- return [
324- ...counts . map ( ( [ name , value ] ) => ( {
338+ // Cumulative series anchor: first observed window's start, stable across
339+ // every subsequent http.requests data point in this export.
340+ state . cumulative . startTimeUnixNano ??= windowStartUnixNano ;
341+ const cumulativeStart = state . cumulative . startTimeUnixNano ;
342+
343+ const sumRecords : JsonObject [ ] = [ ] ;
344+ for ( const [ name , value ] of counts ) {
345+ const seriesKey = `${ name } |${ stableSeriesKey ( event . properties ) } ` ;
346+ const total =
347+ ( state . cumulative . totals . get ( seriesKey ) ?? 0n ) +
348+ BigInt ( Math . trunc ( value ) ) ;
349+ state . cumulative . totals . set ( seriesKey , total ) ;
350+ // Suppress unchanged-from-zero counters to save bytes; receivers
351+ // interpret an absent series as "no events yet".
352+ if ( total === 0n ) {
353+ continue ;
354+ }
355+ sumRecords . push ( {
325356 name : `${ event . eventName } .${ name } ` ,
326357 description : event . eventName ,
327358 unit : "{request}" ,
328359 sum : {
329- aggregationTemporality : AGGREGATION_TEMPORALITY_DELTA ,
360+ aggregationTemporality : AGGREGATION_TEMPORALITY_CUMULATIVE ,
330361 isMonotonic : true ,
331362 dataPoints : [
332363 {
333364 attributes,
334- startTimeUnixNano,
365+ startTimeUnixNano : cumulativeStart ,
335366 timeUnixNano,
336- asInt : String ( Math . trunc ( value ) ) ,
367+ asInt : String ( total ) ,
337368 } ,
338369 ] ,
339370 } ,
340- } ) ) ,
371+ } ) ;
372+ }
373+
374+ return [
375+ ...sumRecords ,
341376 ...toGaugeMetrics (
342377 event ,
343378 gauges ,
344379 attributes ,
345380 timeUnixNano ,
346- startTimeUnixNano ,
381+ windowStartUnixNano ,
347382 ) ,
348383 ] ;
349384}
350385
386+ function stableSeriesKey ( properties : Readonly < Record < string , string > > ) : string {
387+ return Object . entries ( properties )
388+ . sort ( ( [ a ] , [ b ] ) => a . localeCompare ( b ) )
389+ . map ( ( [ k , v ] ) => `${ k } =${ v } ` )
390+ . join ( "|" ) ;
391+ }
392+
351393function toGaugeMetrics (
352394 event : TelemetryEvent ,
353395 measurements : Array < [ string , number ] > ,
0 commit comments