@@ -20,18 +20,32 @@ try {
2020var logFile = logDir + '/cdm-server.log' ;
2121var logStream = fs . createWriteStream ( logFile , { flags : 'a' } ) ;
2222
23- function serverLog ( msg ) {
24- var line = '[' + new Date ( ) . toISOString ( ) + '] ' + msg ;
23+ function serverLog ( msg , reqId ) {
24+ var prefix = '[' + new Date ( ) . toISOString ( ) + ']' ;
25+ if ( reqId ) prefix += ' [' + reqId + ']' ;
26+ var line = prefix + ' ' + msg ;
2527 console . log ( line ) ;
2628 logStream . write ( line + '\n' ) ;
2729}
2830
29- function serverError ( msg ) {
30- var line = '[' + new Date ( ) . toISOString ( ) + '] ERROR: ' + msg ;
31+ function serverError ( msg , reqId ) {
32+ var prefix = '[' + new Date ( ) . toISOString ( ) + ']' ;
33+ if ( reqId ) prefix += ' [' + reqId + ']' ;
34+ var line = prefix + ' ERROR: ' + msg ;
3135 console . error ( line ) ;
3236 logStream . write ( line + '\n' ) ;
3337}
3438
39+ // Per-client request counter for generating short session-like IDs
40+ var clientCounters = { } ;
41+ function generateReqId ( req ) {
42+ var ip = req . ip || req . connection . remoteAddress || 'unknown' ;
43+ var shortIp = ip . replace ( / ^ .* : / , '' ) ; // last part of IPv6 or IPv4
44+ if ( ! clientCounters [ shortIp ] ) clientCounters [ shortIp ] = 0 ;
45+ clientCounters [ shortIp ] ++ ;
46+ return shortIp + '-' + clientCounters [ shortIp ] ;
47+ }
48+
3549function save_host ( host ) {
3650 var host_info = { host : host , header : { 'Content-Type' : 'application/json' } } ;
3751 instances . push ( host_info ) ;
@@ -87,6 +101,12 @@ serverLog('Instance info after discovery: ' + JSON.stringify(instances, null, 2)
87101app . use ( cors ( ) ) ;
88102app . use ( express . json ( ) ) ;
89103
104+ // Assign a request ID to each request for log correlation
105+ app . use ( function ( req , res , next ) {
106+ req . reqId = generateReqId ( req ) ;
107+ next ( ) ;
108+ } ) ;
109+
90110// --------------------------------------------------------------------------------------------------------------
91111// Middleware: resolve a run ID to an OpenSearch instance and yearDotMonth
92112// Attaches req.cdm = { instance, yearDotMonth, runId } on success
@@ -1122,6 +1142,96 @@ app.post('/api/v1/iterations/breakout-values', async (req, res) => {
11221142// Body: { runIds: [...], start, end, source, type, breakout: [...] }
11231143// Returns: { values: { iterationId: { labels: { label: { mean, stddevPct, sampleValues } }, remainingBreakouts: [...] } } }
11241144// When breakout is empty, returns a single label "__all__" with the aggregated value.
1145+ // --------------------------------------------------------------------------------------------------------------
1146+ // POST /api/v1/iterations/period-info — get period IDs and time ranges per iteration
1147+ // Body: { iterations: [{iterationId, runId}], start, end, sampleIndex }
1148+ // Returns: { periods: { iterationId: { periodId, begin, end, runId } } }
1149+ // --------------------------------------------------------------------------------------------------------------
1150+ app . post ( '/api/v1/iterations/period-info' , async ( req , res ) => {
1151+ try {
1152+ const { iterations : reqIterations , start, end, sampleIndex } = req . body ;
1153+ if ( ! Array . isArray ( reqIterations ) || reqIterations . length === 0 ) {
1154+ return res . status ( 400 ) . json ( { code : 'MISSING_PARAMS' , error : 'iterations array is required' } ) ;
1155+ }
1156+ var requestedSampleIdx = ( typeof sampleIndex === 'number' ) ? sampleIndex : null ;
1157+ var perIterSampleIdx = ( typeof sampleIndex === 'object' && sampleIndex !== null && ! Array . isArray ( sampleIndex ) ) ? sampleIndex : null ;
1158+
1159+ getInstancesInfo ( instances ) ;
1160+ var result = { } ;
1161+
1162+ for ( const inst of instances ) {
1163+ if ( invalidInstance ( inst ) ) continue ;
1164+ var ydm = cdm . buildYearDotMonthRange ( inst , 'run' , start || null , end || null ) ;
1165+
1166+ var allIterIds = reqIterations . map ( function ( it ) { return it . iterationId ; } ) ;
1167+ var iterRunIds = reqIterations . map ( function ( it ) { return it . runId ; } ) ;
1168+
1169+ var samples = await cdm . mgetSamples ( inst , allIterIds , ydm ) ;
1170+ var statuses = await cdm . mgetSampleStatuses ( inst , samples || [ ] , ydm ) ;
1171+ if ( typeof statuses === 'undefined' ) statuses = [ ] ;
1172+ var periodNames = await cdm . mgetPrimaryPeriodName ( inst , allIterIds , ydm ) ;
1173+
1174+ var passingSamplesByIter = [ ] ;
1175+ var passingPeriodNamesByIter = [ ] ;
1176+ for ( var i = 0 ; i < allIterIds . length ; i ++ ) {
1177+ var iterSamples = ( samples && samples [ i ] ) || [ ] ;
1178+ var iterStatuses = ( statuses && statuses [ i ] ) || [ ] ;
1179+ var iterPeriodName = ( periodNames && periodNames [ i ] ) || null ;
1180+ var passing = [ ] ;
1181+ for ( var s = 0 ; s < iterSamples . length ; s ++ ) {
1182+ if ( iterStatuses [ s ] === 'pass' ) passing . push ( iterSamples [ s ] ) ;
1183+ }
1184+ passingSamplesByIter . push ( passing ) ;
1185+ passingPeriodNamesByIter . push ( iterPeriodName ) ;
1186+ }
1187+
1188+ var primaryPeriodIds = [ ] ;
1189+ var hasPassing = passingSamplesByIter . some ( function ( s ) { return s . length > 0 ; } ) ;
1190+ if ( hasPassing ) {
1191+ primaryPeriodIds = await cdm . mgetPrimaryPeriodId ( inst , passingSamplesByIter , passingPeriodNamesByIter , ydm ) ;
1192+ if ( typeof primaryPeriodIds === 'undefined' ) primaryPeriodIds = [ ] ;
1193+ }
1194+
1195+ var periodRanges = [ ] ;
1196+ if ( primaryPeriodIds . length > 0 ) {
1197+ periodRanges = await cdm . mgetPeriodRange ( inst , primaryPeriodIds , ydm ) ;
1198+ if ( typeof periodRanges === 'undefined' ) periodRanges = [ ] ;
1199+ }
1200+
1201+ for ( var i = 0 ; i < allIterIds . length ; i ++ ) {
1202+ var iterPeriodIds = ( primaryPeriodIds [ i ] ) || [ ] ;
1203+ var iterRanges = ( periodRanges [ i ] ) || [ ] ;
1204+ if ( iterPeriodIds . length === 0 ) continue ;
1205+
1206+ var selIdx = 0 ;
1207+ if ( perIterSampleIdx && perIterSampleIdx [ allIterIds [ i ] ] != null ) {
1208+ selIdx = perIterSampleIdx [ allIterIds [ i ] ] ;
1209+ } else if ( requestedSampleIdx !== null ) {
1210+ selIdx = requestedSampleIdx ;
1211+ }
1212+ if ( selIdx >= iterPeriodIds . length ) selIdx = 0 ;
1213+
1214+ if ( ! iterPeriodIds [ selIdx ] ) continue ;
1215+ var range = iterRanges [ selIdx ] ;
1216+ if ( ! range || ! range . begin || ! range . end ) continue ;
1217+
1218+ result [ allIterIds [ i ] ] = {
1219+ periodId : iterPeriodIds [ selIdx ] ,
1220+ begin : range . begin ,
1221+ end : range . end ,
1222+ runId : iterRunIds [ i ] ,
1223+ } ;
1224+ }
1225+ }
1226+
1227+ serverLog ( 'POST /api/v1/iterations/period-info: ' + Object . keys ( result ) . length + ' period(s)' ) ;
1228+ res . json ( { periods : result } ) ;
1229+ } catch ( error ) {
1230+ serverError ( 'Error in POST /api/v1/iterations/period-info: ' + error ) ;
1231+ res . status ( 500 ) . json ( { code : 'INTERNAL_ERROR' , error : 'Failed to get period info: ' + error . message } ) ;
1232+ }
1233+ } ) ;
1234+
11251235// --------------------------------------------------------------------------------------------------------------
11261236app . post ( '/api/v1/iterations/supplemental-metric' , async ( req , res ) => {
11271237 try {
@@ -1448,18 +1558,10 @@ app.post('/api/v1/metric-data', async (req, res) => {
14481558 try {
14491559 var { run, period, begin, end, source, type, resolution, breakout, filter, instances : reqInstances } = req . body ;
14501560
1451- serverLog ( '[' + Date . now ( ) + '] Fetching metric data with parameters:' , {
1452- run,
1453- period,
1454- begin,
1455- end,
1456- source,
1457- type,
1458- resolution,
1459- breakout,
1460- filter,
1461- instances : reqInstances ? `${ reqInstances . length } instance(s) provided` : 'using server instances'
1462- } ) ;
1561+ var reqStart = Date . now ( ) ;
1562+ var breakoutStr = Array . isArray ( breakout ) ? breakout . join ( ',' ) : ( breakout || 'none' ) ;
1563+ serverLog ( 'POST /api/v1/metric-data: ' + source + '::' + type + ' resolution=' + resolution + ' breakout=[' + breakoutStr + ']' + ( filter ? ' filter=' + filter : '' ) + ' run=' + ( run || 'none' ) . toString ( ) . substring ( 0 , 8 ) + '... period=' + ( period || 'none' ) . toString ( ) . substring ( 0 , 8 ) + '...' , req . reqId ) ;
1564+ serverLog ( ' curl: curl -s -X POST http://localhost:3000/api/v1/metric-data -H "Content-Type: application/json" -d \'' + JSON . stringify ( { run : run , period : period , begin : begin , end : end , source : source , type : type , resolution : resolution , breakout : breakout , filter : filter } ) + '\'' , req . reqId ) ;
14631565
14641566 // Use instances from request if provided, otherwise use server's configured instances
14651567 var instancesToUse = reqInstances && reqInstances . length > 0 ? reqInstances : instances ;
@@ -1537,15 +1639,9 @@ app.post('/api/v1/metric-data', async (req, res) => {
15371639 }
15381640 metric_data = resp [ 'data-sets' ] [ 0 ] ;
15391641
1540- console . log (
1541- '[' +
1542- Date . now ( ) +
1543- '] Request completed from Opensearch instance: ' +
1544- instance [ 'host' ] +
1545- ' and cdm: ' +
1546- instance [ 'ver' ] +
1547- '\n'
1548- ) ;
1642+ var labelCount = metric_data && metric_data . values ? Object . keys ( metric_data . values ) . length : 0 ;
1643+ var elapsed = Date . now ( ) - reqStart ;
1644+ serverLog ( 'POST /api/v1/metric-data: ' + source + '::' + type + ' -> ' + labelCount + ' label(s) in ' + elapsed + 'ms' , req . reqId ) ;
15491645
15501646 // Return the data
15511647 res . json ( metric_data ) ;
0 commit comments