11import { Logger } from "@trigger.dev/core/logger" ;
22import { tryCatch } from "@trigger.dev/core/utils" ;
3+ import { getMeter , type Counter , type Histogram , type Meter } from "@internal/tracing" ;
34import { nanoid } from "nanoid" ;
45import pLimit from "p-limit" ;
56import { signalsEmitter } from "~/services/signals.server" ;
@@ -16,6 +17,11 @@ export type DynamicFlushSchedulerConfig<T> = {
1617 loadSheddingThreshold ?: number ; // Number of items that triggers load shedding
1718 loadSheddingEnabled ?: boolean ;
1819 isDroppableEvent ?: ( item : T ) => boolean ; // Function to determine if an event can be dropped
20+ // Self-observability. `name` is the low-cardinality `scheduler` label that separates the
21+ // task_events / llm_metrics / otlp_metrics instances in the same process. `meter` defaults to
22+ // the global provider; inject one in tests. Instruments are no-op unless metrics are enabled.
23+ meter ?: Meter ;
24+ name ?: string ;
1925} ;
2026
2127export class DynamicFlushScheduler < T > {
@@ -54,7 +60,21 @@ export class DynamicFlushScheduler<T> {
5460
5561 private readonly logger : Logger = new Logger ( "EventRepo.DynamicFlushScheduler" , "info" ) ;
5662
63+ // Pre-allocated attribute objects (closed label sets) so the hot flush path never allocates.
64+ private readonly _metricAttrs : { scheduler : string } ;
65+ private readonly _batchOkAttrs : { scheduler : string ; outcome : string } ;
66+ private readonly _batchFailedAttrs : { scheduler : string ; outcome : string } ;
67+ private _batchesCounter ?: Counter ;
68+ private _itemsCounter ?: Counter ;
69+ private _flushDurationHistogram ?: Histogram ;
70+ private _batchSizeHistogram ?: Histogram ;
71+ private _droppedEventsCounter ?: Counter ;
72+
5773 constructor ( config : DynamicFlushSchedulerConfig < T > ) {
74+ const schedulerName = config . name ?? "unknown" ;
75+ this . _metricAttrs = { scheduler : schedulerName } ;
76+ this . _batchOkAttrs = { scheduler : schedulerName , outcome : "ok" } ;
77+ this . _batchFailedAttrs = { scheduler : schedulerName , outcome : "failed" } ;
5878 this . batchQueue = [ ] ;
5979 this . currentBatch = [ ] ;
6080 this . BATCH_SIZE = config . batchSize ;
@@ -80,6 +100,54 @@ export class DynamicFlushScheduler<T> {
80100 this . startFlushTimer ( ) ;
81101 this . startMetricsReporter ( ) ;
82102 this . setupShutdownHandlers ( ) ;
103+ this . #setupOtelMetrics( config . meter , schedulerName ) ;
104+ }
105+
106+ #setupOtelMetrics( meterOverride : Meter | undefined , name : string ) : void {
107+ const meter = meterOverride ?? getMeter ( "ingest-flush" ) ;
108+
109+ this . _batchesCounter = meter . createCounter ( "ingest.flush.batches" , {
110+ description : "Batches flushed to the sink, by outcome" ,
111+ unit : "batches" ,
112+ } ) ;
113+ this . _itemsCounter = meter . createCounter ( "ingest.flush.items" , {
114+ description : "Items successfully flushed to the sink" ,
115+ unit : "items" ,
116+ } ) ;
117+ this . _flushDurationHistogram = meter . createHistogram ( "ingest.flush.duration" , {
118+ description : "Wall-clock duration of a single batch flush" ,
119+ unit : "ms" ,
120+ } ) ;
121+ this . _batchSizeHistogram = meter . createHistogram ( "ingest.flush.batch_size" , {
122+ description : "Number of items in a flushed batch" ,
123+ unit : "items" ,
124+ } ) ;
125+ this . _droppedEventsCounter = meter . createCounter ( "ingest.flush.dropped_events" , {
126+ description : "Events dropped by load shedding before they reached the sink" ,
127+ unit : "events" ,
128+ } ) ;
129+
130+ // Pull-based gauges: read at export time only, so they add zero hot-path cost.
131+ const queueDepthGauge = meter . createObservableGauge ( "ingest.flush.queue_depth" , {
132+ description : "Items queued and awaiting flush" ,
133+ unit : "items" ,
134+ } ) ;
135+ const concurrencyGauge = meter . createObservableGauge ( "ingest.flush.concurrency" , {
136+ description : "Current concurrent-flush limit" ,
137+ unit : "flushes" ,
138+ } ) ;
139+ const loadSheddingGauge = meter . createObservableGauge ( "ingest.flush.load_shedding" , {
140+ description : "1 while actively shedding load, otherwise 0" ,
141+ } ) ;
142+
143+ meter . addBatchObservableCallback (
144+ ( result ) => {
145+ result . observe ( queueDepthGauge , this . totalQueuedItems , this . _metricAttrs ) ;
146+ result . observe ( concurrencyGauge , this . limiter . concurrency , this . _metricAttrs ) ;
147+ result . observe ( loadSheddingGauge , this . isLoadShedding ? 1 : 0 , this . _metricAttrs ) ;
148+ } ,
149+ [ queueDepthGauge , concurrencyGauge , loadSheddingGauge ]
150+ ) ;
83151 }
84152
85153 addToBatch ( items : T [ ] ) : void {
@@ -92,6 +160,7 @@ export class DynamicFlushScheduler<T> {
92160
93161 if ( dropped . length > 0 ) {
94162 this . metrics . droppedEvents += dropped . length ;
163+ this . _droppedEventsCounter ?. add ( dropped . length , this . _metricAttrs ) ;
95164
96165 // Track dropped events by kind if possible
97166 dropped . forEach ( ( item ) => {
@@ -213,6 +282,11 @@ export class DynamicFlushScheduler<T> {
213282 self . metrics . flushedBatches ++ ;
214283 self . metrics . totalItemsFlushed += itemCount ;
215284
285+ self . _flushDurationHistogram ?. record ( duration , self . _metricAttrs ) ;
286+ self . _batchSizeHistogram ?. record ( itemCount , self . _metricAttrs ) ;
287+ self . _itemsCounter ?. add ( itemCount , self . _metricAttrs ) ;
288+ self . _batchesCounter ?. add ( 1 , self . _batchOkAttrs ) ;
289+
216290 self . logger . debug ( "Batch flushed successfully" , {
217291 flushId,
218292 itemCount,
@@ -253,6 +327,7 @@ export class DynamicFlushScheduler<T> {
253327 this . logger . error ( "Error flushing batch" , {
254328 error : flushError ,
255329 } ) ;
330+ this . _batchesCounter ?. add ( 1 , this . _batchFailedAttrs ) ;
256331 }
257332 } )
258333 ) ;
0 commit comments