@@ -107,6 +107,69 @@ function normalizePath(urlPath, matchers) {
107107 return { route, params : { } } ;
108108}
109109
110+ // Prefer proxy-provided client IPs when available.
111+ function sanitizeIp ( rawIp ) {
112+ if ( ! rawIp ) return '' ;
113+
114+ let ip = String ( rawIp ) . trim ( ) ;
115+ if ( ! ip ) return '' ;
116+
117+ // Handle IPv4-mapped IPv6 values (e.g. ::ffff:192.168.0.1).
118+ if ( ip . startsWith ( '::ffff:' ) ) {
119+ ip = ip . slice ( '::ffff:' . length ) ;
120+ }
121+
122+ // Handle bracketed IPv6 with port (e.g. [2001:db8::1]:12345).
123+ const bracketedIpv6 = ip . match ( / ^ \[ ( [ ^ \] ] + ) \] (?: : \d + ) ? $ / ) ;
124+ if ( bracketedIpv6 ) {
125+ ip = bracketedIpv6 [ 1 ] ;
126+ }
127+
128+ // Handle IPv4 with port (e.g. 192.168.0.1:12345).
129+ const ipv4WithPort = ip . match ( / ^ ( \d { 1 , 3 } (?: \. \d { 1 , 3 } ) { 3 } ) : \d + $ / ) ;
130+ if ( ipv4WithPort ) {
131+ ip = ipv4WithPort [ 1 ] ;
132+ }
133+
134+ return ip ;
135+ }
136+
137+ function getClientIp ( req ) {
138+ const xForwardedFor = req . headers && req . headers [ 'x-forwarded-for' ] ;
139+
140+ if ( typeof xForwardedFor === 'string' && xForwardedFor . trim ( ) ) {
141+ // X-Forwarded-For can be a list: client, proxy1, proxy2
142+ return sanitizeIp ( xForwardedFor . split ( ',' ) [ 0 ] ) ;
143+ }
144+
145+ if ( Array . isArray ( xForwardedFor ) && xForwardedFor . length > 0 ) {
146+ return sanitizeIp ( String ( xForwardedFor [ 0 ] ) . split ( ',' ) [ 0 ] ) ;
147+ }
148+
149+ return sanitizeIp (
150+ req . ip
151+ || ( req . socket && req . socket . remoteAddress )
152+ || ( req . connection && req . connection . remoteAddress )
153+ || ''
154+ ) ;
155+ }
156+
157+ function anonymizeIp ( ip ) {
158+ if ( ! ip ) return 'Unknown' ;
159+
160+ if ( ip . includes ( '.' ) ) {
161+ const octets = ip . split ( '.' ) ;
162+ if ( octets . length >= 2 ) return `${ octets [ 0 ] } .${ octets [ 1 ] } .0.0` ;
163+ }
164+
165+ if ( ip . includes ( ':' ) ) {
166+ const parts = ip . split ( ':' ) . filter ( Boolean ) ;
167+ if ( parts . length >= 2 ) return `${ parts [ 0 ] } :${ parts [ 1 ] } ::` ;
168+ }
169+
170+ return 'Unknown' ;
171+ }
172+
110173// ---------------------------------------------------------------------------
111174// Public API
112175// ---------------------------------------------------------------------------
@@ -121,29 +184,26 @@ function metricsMiddleware(basePaths = ['/rest/current', '/rest/v1'], debug = fa
121184
122185 return function trackMetrics ( req , res , next ) {
123186 const startMs = Date . now ( ) ;
124-
125- // IP and Geolocation logic
126- const ip = req . ip || ( req . connection && req . connection . remoteAddress ) || '' ;
127- if ( debug ) console . log ( 'ip' , ip ) ;
128- const geo = geoip . lookup ( ip ) ;
129- if ( debug ) console . log ( 'geo' , geo ) ;
130-
131- req . geoStats = {
132- country : geo ? geo . country : 'Unknown' ,
133- region : geo ? geo . region : 'Unknown' ,
134- city : geo ? geo . city : 'Unknown' ,
135- // Anonymize IP by keeping only the first two octets (e.g. 192.168.x.x)
136- // This way we can still get some geographic info without needing consent
137- anonIp : ip . length > 0 ? ip . split ( '.' ) . slice ( 0 , 2 ) . join ( '.' ) + '.0.0' : 'Unknown'
138- } ;
139- if ( debug ) console . log ( 'ip' , req . geoStats ) ;
140-
141187 // Capture the full path NOW — req.path is mutated by Express after sub-router
142188 // dispatch, but req.originalUrl is always the original unmodified path.
143189 const fullPath = req . originalUrl . split ( '?' ) [ 0 ] ;
144190 const isFaviconRequest = fullPath . includes ( 'favicon' ) ;
145191 const print = debug && ! isFaviconRequest ;
146192 if ( print ) console . log ( `Received request: ${ req . method } ${ fullPath } , path ${ req . path } , url ${ req . url } ` )
193+
194+ // IP and Geolocation logic
195+ const ip = getClientIp ( req ) ;
196+ if ( debug ) console . log ( 'Client IP' , ip ) ;
197+ const geo = geoip . lookup ( ip ) ;
198+ if ( debug ) console . log ( 'Geo Data' , geo ) ;
199+
200+ req . geoStats = {
201+ country : geo ? geo . country : '' ,
202+ region : geo ? geo . region : '' ,
203+ city : geo ? geo . city : '' ,
204+ // Keep a coarse-grained anonymized IP in labels.
205+ anonIp : anonymizeIp ( ip )
206+ } ;
147207
148208 res . on ( 'finish' , ( ) => {
149209 const { route, params } = normalizePath ( fullPath , matchers ) ;
@@ -158,7 +218,8 @@ function metricsMiddleware(basePaths = ['/rest/current', '/rest/v1'], debug = fa
158218 status_code : String ( res . statusCode ) ,
159219 ...params
160220 } ;
161- if ( print ) console . log ( 'labels' , labels ) ;
221+ if ( print ) console . log ( 'Labels:' , labels ) ;
222+ if ( print ) console . log ( 'geoStats' , req . geoStats ) ;
162223 httpRequestsTotal . inc ( labels ) ;
163224 httpRequestDuration . observe ( labels , ( Date . now ( ) - startMs ) / 1000 ) ;
164225 httpGeoRequestsTotal . inc ( {
0 commit comments