Skip to content

Commit 52ca68f

Browse files
authored
Merge pull request #122 from huggingface/claude/add-ip-session-tracking-01FU71ksN1sabcwQm8UfjWu2
Add IP address tracking to session logs
2 parents ee55e83 + 2ed9da0 commit 52ca68f

File tree

8 files changed

+98
-15
lines changed

8 files changed

+98
-15
lines changed

packages/app/src/server/transport/base-transport.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export interface SessionMetadata {
5959
};
6060
pingFailures?: number;
6161
lastPingAttempt?: Date;
62+
ipAddress?: string;
6263
}
6364

6465
/**
@@ -164,6 +165,39 @@ export abstract class BaseTransport {
164165
this.metrics.trackNewConnection();
165166
}
166167

168+
/**
169+
* Track an IP address for unique IP metrics
170+
*/
171+
protected trackIpAddress(ipAddress: string | undefined): void {
172+
this.metrics.trackIpAddress(ipAddress);
173+
}
174+
175+
/**
176+
* Extract IP address from request headers
177+
* Handles x-forwarded-for, x-real-ip, and direct IP
178+
*/
179+
protected extractIpAddress(
180+
headers: Record<string, string | string[] | undefined>,
181+
directIp?: string
182+
): string | undefined {
183+
// Try x-forwarded-for first (most reliable for proxied traffic)
184+
const forwardedFor = headers['x-forwarded-for'];
185+
if (forwardedFor) {
186+
// x-forwarded-for can be a comma-separated list, take the first one (original client)
187+
const ip = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor;
188+
return ip?.split(',')[0]?.trim();
189+
}
190+
191+
// Try x-real-ip (nginx)
192+
const realIp = headers['x-real-ip'];
193+
if (realIp) {
194+
return Array.isArray(realIp) ? realIp[0] : realIp;
195+
}
196+
197+
// Fallback to direct IP (no proxy scenario)
198+
return directIp;
199+
}
200+
167201
/**
168202
* Associate a session with a client identity when client info becomes available
169203
*/

packages/app/src/server/transport/sse-transport.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ export class SseTransport extends StatefulTransport<SSEConnection> {
7878
const headers = req.headers as Record<string, string>;
7979
extractQueryParamsToHeaders(req, headers);
8080

81+
// Extract IP address for tracking
82+
const ipAddress = this.extractIpAddress(req.headers, req.ip);
83+
this.trackIpAddress(ipAddress);
84+
8185
// Validate auth and track metrics
8286
const authResult = await this.validateAuthAndTrackMetrics(headers);
8387
if (!authResult.shouldContinue) {
@@ -107,6 +111,7 @@ export class SseTransport extends StatefulTransport<SSEConnection> {
107111
isAuthenticated,
108112
requestJson: req.body ?? {},
109113
capabilities: requestBody?.params?.capabilities,
114+
ipAddress,
110115
});
111116

112117
// Create comprehensive cleanup function
@@ -124,6 +129,7 @@ export class SseTransport extends StatefulTransport<SSEConnection> {
124129
requestCount: 0,
125130
isAuthenticated: authResult.shouldContinue && !!headers['authorization'],
126131
capabilities: {},
132+
ipAddress,
127133
},
128134
cleaningUp: false,
129135
};

packages/app/src/server/transport/stateless-http-transport.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ export class StatelessHttpTransport extends BaseTransport {
147147
// Check HF token validity if present
148148
const headers = req.headers as Record<string, string>;
149149
extractQueryParamsToHeaders(req, headers);
150+
151+
// Extract IP address for tracking
152+
const ipAddress = this.extractIpAddress(req.headers, req.ip);
153+
this.trackIpAddress(ipAddress);
154+
150155
// Extract method name for tracking using shared utility
151156
const requestBody = req.body as
152157
| { method?: string; params?: { clientInfo?: unknown; capabilities?: unknown; name?: string } }
@@ -168,7 +173,7 @@ export class StatelessHttpTransport extends BaseTransport {
168173
if (requestBody?.method === 'initialize') {
169174
// Create new session
170175
sessionId = randomUUID();
171-
this.createAnalyticsSession(sessionId, authResult.userIdentified);
176+
this.createAnalyticsSession(sessionId, authResult.userIdentified, ipAddress);
172177

173178
// Add session ID to response headers
174179
res.setHeader('Mcp-Session-Id', sessionId);
@@ -182,6 +187,7 @@ export class StatelessHttpTransport extends BaseTransport {
182187
clientVersion: initClientInfo?.version,
183188
requestJson: requestBody.params || '{}',
184189
capabilities: requestBody?.params?.capabilities,
190+
ipAddress,
185191
});
186192
} else if (sessionId) {
187193
// Try to resume existing session
@@ -441,6 +447,7 @@ export class StatelessHttpTransport extends BaseTransport {
441447
clientName: analyticsSession?.metadata.clientInfo?.name,
442448
clientVersion: analyticsSession?.metadata.clientInfo?.version,
443449
requestJson: { method: 'session_delete', sessionId },
450+
ipAddress: analyticsSession?.metadata.ipAddress,
444451
});
445452

446453
res.status(200).json({ jsonrpc: '2.0', result: { deleted: true } });
@@ -492,7 +499,7 @@ export class StatelessHttpTransport extends BaseTransport {
492499
}
493500

494501
// Analytics mode methods
495-
private createAnalyticsSession(sessionId: string, isAuthenticated: boolean): void {
502+
private createAnalyticsSession(sessionId: string, isAuthenticated: boolean, ipAddress?: string): void {
496503
const session: AnalyticsSession = {
497504
transport: null,
498505
server: null, // Server is null in analytics mode
@@ -503,6 +510,7 @@ export class StatelessHttpTransport extends BaseTransport {
503510
requestCount: 1,
504511
isAuthenticated,
505512
capabilities: {},
513+
ipAddress,
506514
},
507515
};
508516

packages/app/src/server/utils/query-logger.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ export function logSystemEvent(
266266
clientVersion?: string;
267267
requestJson?: unknown;
268268
capabilities?: unknown;
269+
ipAddress?: string;
269270
}
270271
): void {
271272
if (!systemLogger) {
@@ -299,6 +300,9 @@ export function logSystemEvent(
299300
version: options?.clientVersion || capabilitiesVersion || null,
300301
authorized: options?.isAuthenticated ?? false, // renamed from isAuthenticated
301302

303+
// IP address for session tracking
304+
ipAddress: options?.ipAddress || null,
305+
302306
// Full request data for context
303307
capabilities: options?.capabilities ? JSON.stringify(options.capabilities) : null,
304308
clientSessionId: options?.clientSessionId || null,

packages/app/src/server/web-server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ export class WebServer {
319319
connectionStatus,
320320
pingFailures: session.pingFailures || 0,
321321
lastPingAttempt: session.lastPingAttempt?.toISOString(),
322+
ipAddress: session.ipAddress,
322323
};
323324
});
324325

packages/app/src/shared/transport-metrics.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface TransportMetrics {
1515
anonymous: number;
1616
unauthorized?: number; // 401 errors
1717
cleaned?: number; // Only for stateful transports
18+
uniqueIps?: number; // Unique IP addresses that have connected
1819
};
1920

2021
// Session lifecycle metrics (only for stateful transports)
@@ -155,6 +156,7 @@ export interface SessionData {
155156
connectionStatus?: 'Connected' | 'Distressed' | 'Disconnected';
156157
pingFailures?: number;
157158
lastPingAttempt?: string; // ISO date string
159+
ipAddress?: string;
158160
}
159161

160162
export interface TransportMetricsResponse {
@@ -415,12 +417,14 @@ export class MetricsCounter {
415417
private rollingMinute: RollingWindowCounter;
416418
private rollingHour: RollingWindowCounter;
417419
private rolling3Hours: RollingWindowCounter;
420+
private uniqueIps: Set<string>;
418421

419422
constructor() {
420423
this.metrics = createEmptyMetrics();
421424
this.rollingMinute = new RollingWindowCounter(1);
422425
this.rollingHour = new RollingWindowCounter(60);
423426
this.rolling3Hours = new RollingWindowCounter(180);
427+
this.uniqueIps = new Set();
424428
}
425429

426430
/**
@@ -430,15 +434,18 @@ export class MetricsCounter {
430434
// Calculate rates (requests per minute) for each window
431435
// Note: All values represent "requests per minute" calculated over their respective windows
432436
this.metrics.requests.lastMinute = this.rollingMinute.getCount(); // Requests in last 1 minute (already per minute)
433-
437+
434438
// For longer windows, divide total count by window size to get per-minute rate
435439
const hourCount = this.rollingHour.getCount();
436440
const threeHourCount = this.rolling3Hours.getCount();
437-
441+
438442
// Calculate per-minute rates for the longer windows
439443
this.metrics.requests.lastHour = Math.round((hourCount / 60) * 100) / 100; // Requests per minute over last hour
440444
this.metrics.requests.last3Hours = Math.round((threeHourCount / 180) * 100) / 100; // Requests per minute over last 3 hours
441-
445+
446+
// Update unique IPs count
447+
this.metrics.connections.uniqueIps = this.uniqueIps.size;
448+
442449
return this.metrics;
443450
}
444451

@@ -493,6 +500,15 @@ export class MetricsCounter {
493500
this.metrics.connections.total++;
494501
}
495502

503+
/**
504+
* Track an IP address
505+
*/
506+
trackIpAddress(ipAddress: string | undefined): void {
507+
if (ipAddress) {
508+
this.uniqueIps.add(ipAddress);
509+
}
510+
}
511+
496512
/**
497513
* Update active connection count
498514
*/

packages/app/src/web/components/StatefulTransportMetrics.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type SessionData = {
2222
connectionStatus?: 'Connected' | 'Distressed' | 'Disconnected';
2323
pingFailures?: number;
2424
lastPingAttempt?: string;
25+
ipAddress?: string;
2526
};
2627

2728
/**
@@ -171,6 +172,15 @@ export function StatefulTransportMetrics({ metrics }: StatefulTransportMetricsPr
171172
header: createSortableHeader('Last Activity'),
172173
cell: ({ row }) => <div className="text-sm">{formatRelativeTime(row.getValue<string>('lastActivity'))}</div>,
173174
},
175+
{
176+
accessorKey: 'ipAddress',
177+
header: createSortableHeader('IP Address'),
178+
cell: ({ row }) => (
179+
<div className="font-mono text-sm" title={row.getValue<string>('ipAddress') || 'Unknown'}>
180+
{row.getValue<string>('ipAddress') || '-'}
181+
</div>
182+
),
183+
},
174184
];
175185

176186
return (
@@ -250,6 +260,14 @@ export function StatefulTransportMetrics({ metrics }: StatefulTransportMetricsPr
250260
{metrics.requests.averagePerMinute}/{metrics.requests.last3Hours}/{metrics.requests.lastHour}
251261
</TableCell>
252262
</TableRow>
263+
<TableRow>
264+
<TableCell className="font-medium text-sm">Unique IPs</TableCell>
265+
<TableCell className="text-sm font-mono">{metrics.connections.uniqueIps ?? 0}</TableCell>
266+
<TableCell className="font-medium text-sm">Client/Server Errors (4xx/5xx)</TableCell>
267+
<TableCell className="text-sm font-mono">
268+
{metrics.errors.expected}/{metrics.errors.unexpected}
269+
</TableCell>
270+
</TableRow>
253271
{metrics.sessionLifecycle && (
254272
<TableRow>
255273
<TableCell className="font-medium text-sm">Sessions New/Res-fail/Del</TableCell>
@@ -258,12 +276,6 @@ export function StatefulTransportMetrics({ metrics }: StatefulTransportMetricsPr
258276
</TableCell>
259277
</TableRow>
260278
)}
261-
<TableRow>
262-
<TableCell className="font-medium text-sm">Client Errors (4xx)</TableCell>
263-
<TableCell className="text-sm font-mono">{metrics.errors.expected}</TableCell>
264-
<TableCell className="font-medium text-sm">Server Errors (5xx)</TableCell>
265-
<TableCell className="text-sm font-mono">{metrics.errors.unexpected}</TableCell>
266-
</TableRow>
267279
{metrics.pings && (
268280
<TableRow>
269281
<TableCell className="font-medium text-sm">Pings Sent</TableCell>

packages/app/src/web/components/StatelessTransportMetrics.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,12 @@ export function StatelessTransportMetrics({ metrics }: StatelessTransportMetrics
180180
</TableCell>
181181
</TableRow>
182182
<TableRow>
183-
<TableCell className="font-medium text-sm">Client Errors (4xx)</TableCell>
184-
<TableCell className="text-sm font-mono">{metrics.errors.expected}</TableCell>
185-
<TableCell className="font-medium text-sm">Server Errors (5xx)</TableCell>
186-
<TableCell className="text-sm font-mono">{metrics.errors.unexpected}</TableCell>
183+
<TableCell className="font-medium text-sm">Unique IPs</TableCell>
184+
<TableCell className="text-sm font-mono">{metrics.connections.uniqueIps ?? 0}</TableCell>
185+
<TableCell className="font-medium text-sm">Client/Server Errors (4xx/5xx)</TableCell>
186+
<TableCell className="text-sm font-mono">
187+
{metrics.errors.expected}/{metrics.errors.unexpected}
188+
</TableCell>
187189
</TableRow>
188190
{(metrics.staticPageHits200 !== undefined || metrics.staticPageHits405 !== undefined) && (
189191
<TableRow>

0 commit comments

Comments
 (0)