Skip to content

Commit 800166c

Browse files
committed
Add server statistics tracking and improve README clarity
- Introduced ServerStats and HTTP2Stats classes for tracking connection and request statistics. - Updated README to clarify statistics reset behavior. - Added debug logging method in HttpConnection for better request tracking. - Enhanced ServerConfig with new HTTP/2 connection window size configuration. - properly support http2 receive window
1 parent df9de08 commit 800166c

File tree

8 files changed

+291
-234
lines changed

8 files changed

+291
-234
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ Idle Closes: 0
240240
Reply Errors: 0
241241
```
242242

243-
The counts can be reset using `/__stats?reset`. The `requests/sec` is calculated from the previous statistics request.
243+
The counts and rates for non "Total" statistics are reset with each pull of the statistics.
244244

245245
## maven
246246

src/main/java/robaho/net/httpserver/HttpConnection.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ public boolean isClosed() {
7474
connectionId = "["+socket.getLocalPort()+"."+socket.getPort()+"]";
7575
}
7676

77+
public void debug() {
78+
logger.log(Level.INFO,toString()+", inRequest "+inRequest+", request count "+requestCount.get());
79+
}
80+
7781
SSLSession getSSLSession() {
7882
return (socket instanceof SSLSocket ssl) ? ssl.getHandshakeSession() : null;
7983
}
@@ -113,7 +117,7 @@ synchronized void close() {
113117
if(requestCount.get()==0) {
114118
logger.log(Level.WARNING, "closing connection: remote "+socket.getRemoteSocketAddress() + " with 0 requests");
115119
} else {
116-
logger.log(Level.TRACE, () -> "Closing connection: remote " + socket.getRemoteSocketAddress());
120+
logger.log(Level.TRACE, () -> "closing connection: remote " + socket.getRemoteSocketAddress());
117121
}
118122
}
119123

src/main/java/robaho/net/httpserver/ServerConfig.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public class ServerConfig {
5252

5353
private static final int DEFAULT_HTTP2_MAX_FRAME_SIZE = 16384;
5454
private static final int DEFAULT_HTTP2_INITIAL_WINDOW_SIZE = 65535;
55+
private static final int DEFAULT_HTTP2_CONNECTION_WINDOW_SIZE = 65535;
5556
private static final int DEFAULT_HTTP2_MAX_CONCURRENT_STREAMS = -1; // use -1 for no limit
5657

5758
private static long idleTimerScheduleMillis;
@@ -79,6 +80,7 @@ public class ServerConfig {
7980
private static boolean http2OverNonSSL;
8081
private static int http2MaxFrameSize;
8182
private static int http2InitialWindowSize;
83+
private static int http2ConnectionWindowSize;
8284
private static int http2MaxConcurrentStreams;
8385
private static boolean http2DisableFlushDelay;
8486

@@ -142,6 +144,7 @@ public Void run() {
142144

143145
http2MaxFrameSize = Integer.getInteger(pkg + ".http2MaxFrameSize", DEFAULT_HTTP2_MAX_FRAME_SIZE);
144146
http2InitialWindowSize = Integer.getInteger(pkg + ".http2InitialWindowSize", DEFAULT_HTTP2_INITIAL_WINDOW_SIZE);
147+
http2ConnectionWindowSize = Integer.getInteger(pkg + ".http2ConnectionWindowSize", DEFAULT_HTTP2_CONNECTION_WINDOW_SIZE);
145148

146149
http2MaxConcurrentStreams = Integer.getInteger(pkg + ".http2MaxConcurrentStreams", DEFAULT_HTTP2_MAX_CONCURRENT_STREAMS);
147150
http2DisableFlushDelay = Boolean.getBoolean(pkg + ".http2DisableFlushDelay");
@@ -249,6 +252,9 @@ public static int http2MaxFrameSize() {
249252
public static int http2InitialWindowSize() {
250253
return http2InitialWindowSize;
251254
}
255+
public static int http2ConnectionWindowSize() {
256+
return http2ConnectionWindowSize;
257+
}
252258
/**
253259
* @return the maximum number of concurrent streams per connection, or -1 for no limit
254260
*/

src/main/java/robaho/net/httpserver/ServerImpl.java

Lines changed: 58 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
import java.util.concurrent.Executor;
5252
import java.util.concurrent.ExecutorService;
5353
import java.util.concurrent.Executors;
54-
import java.util.concurrent.atomic.AtomicLong;
5554
import java.util.logging.LogRecord;
5655

5756
import javax.net.ssl.SSLSocket;
@@ -68,6 +67,7 @@
6867
import robaho.net.httpserver.http2.HTTP2Connection;
6968
import robaho.net.httpserver.http2.HTTP2ErrorCode;
7069
import robaho.net.httpserver.http2.HTTP2Exception;
70+
import robaho.net.httpserver.http2.HTTP2Stats;
7171
import robaho.net.httpserver.http2.HTTP2Stream;
7272

7373
/**
@@ -119,13 +119,8 @@ class ServerImpl {
119119
private Thread dispatcherThread;
120120

121121
// statistics
122-
private final AtomicLong connectionCount = new AtomicLong();
123-
private final AtomicLong requestCount = new AtomicLong();
124-
private final AtomicLong handleExceptionCount = new AtomicLong();
125-
private final AtomicLong socketExceptionCount = new AtomicLong();
126-
private final AtomicLong idleCloseCount = new AtomicLong();
127-
private final AtomicLong replyErrorCount = new AtomicLong();
128-
private final AtomicLong maxConnectionsExceededCount = new AtomicLong();
122+
private final ServerStats stats = new ServerStats();
123+
private final HTTP2Stats http2Stats = new HTTP2Stats();
129124

130125
ServerImpl(HttpServer wrapper, String protocol, InetSocketAddress addr, int backlog) throws IOException {
131126

@@ -156,55 +151,41 @@ class ServerImpl {
156151
if(Boolean.getBoolean("robaho.net.httpserver.EnableStats")) {
157152
createContext("/__stats",new StatsHandler());
158153
}
154+
if(Boolean.getBoolean("robaho.net.httpserver.EnableDebug")) {
155+
createContext("/__debug",new DebugHandler());
156+
}
159157
}
160158

161159
private class StatsHandler implements HttpHandler {
162-
volatile long lastStatsTime = System.currentTimeMillis();
163-
volatile long lastRequestCount = 0;
164160
@Override
165161
public void handle(HttpExchange exchange) throws IOException {
166-
167-
long now = System.currentTimeMillis();
168-
169-
if("reset".equals(exchange.getRequestURI().getQuery())) {
170-
connectionCount.set(0);
171-
requestCount.set(0);
172-
handleExceptionCount.set(0);
173-
socketExceptionCount.set(0);
174-
idleCloseCount.set(0);
175-
replyErrorCount.set(0);
176-
maxConnectionsExceededCount.set(0);
177-
lastStatsTime = now;
178-
lastRequestCount = 0;
179-
exchange.sendResponseHeaders(200,-1);
180-
exchange.close();
181-
return;
182-
}
183-
184-
var rc = requestCount.get();
185-
186162
var output =
187163
(
188-
"Connections: "+connectionCount.get()+"\n" +
189164
"Active Connections: "+allConnections.size()+"\n" +
190-
"Requests: "+rc+"\n" +
191-
"Requests/sec: "+(long)((rc-lastRequestCount)/(((double)(now-lastStatsTime))/1000))+"\n"+
192-
"Handler Exceptions: "+handleExceptionCount.get()+"\n"+
193-
"Socket Exceptions: "+socketExceptionCount.get()+"\n"+
194-
"Mac Connections Exceeded: "+maxConnectionsExceededCount.get()+"\n"+
195-
"Idle Closes: "+idleCloseCount.get()+"\n"+
196-
"Reply Errors: "+replyErrorCount.get()+"\n"
165+
stats.stats()+
166+
http2Stats.stats()
197167
).getBytes();
198168

199-
lastStatsTime = now;
200-
lastRequestCount = rc;
201-
202169
exchange.sendResponseHeaders(200,output.length);
203170
exchange.getResponseBody().write(output);
204171
exchange.getResponseBody().close();
205172
}
206173
}
207174

175+
/** log state to assist debugging */
176+
private class DebugHandler implements HttpHandler {
177+
@Override
178+
public void handle(HttpExchange exchange) throws IOException {
179+
logger.log(Level.INFO,"logging debug state, requestor "+exchange.getRemoteAddress());
180+
for(var connection : allConnections) {
181+
connection.debug();
182+
}
183+
Http2Exchange.debug();
184+
exchange.sendResponseHeaders(200,-1);
185+
}
186+
}
187+
188+
208189
public void bind(InetSocketAddress addr, int backlog) throws IOException {
209190
if (bound) {
210191
throw new BindException("HttpServer already bound");
@@ -365,12 +346,12 @@ public void run() {
365346
if(logger.isLoggable(Level.TRACE)) {
366347
logger.log(Level.TRACE, "accepted connection: " + s.toString());
367348
}
368-
connectionCount.incrementAndGet();
349+
stats.connectionCount.incrementAndGet();
369350
if (MAX_CONNECTIONS > 0 && allConnections.size() >= MAX_CONNECTIONS) {
370351
// we've hit max limit of current open connections, so we go
371352
// ahead and close this connection without processing it
372353
try {
373-
maxConnectionsExceededCount.incrementAndGet();
354+
stats.maxConnectionsExceededCount.incrementAndGet();
374355
logger.log(Level.WARNING, "closing accepted connection due to too many connections");
375356
s.close();
376357
} catch (IOException ignore) {
@@ -429,7 +410,7 @@ public void run() {
429410

430411
} catch (Exception e) {
431412
logger.log(Level.TRACE, "Dispatcher Exception", e);
432-
handleExceptionCount.incrementAndGet();
413+
stats.handleExceptionCount.incrementAndGet();
433414
closeConnection(c);
434415
}
435416
} catch (IOException e) {
@@ -473,12 +454,23 @@ class Http2Exchange implements Runnable,HTTP2Connection.StreamHandler {
473454
final String protocol;
474455

475456
private static final Set<Http2Exchange> allHttp2Exchanges = Collections.newSetFromMap(new ConcurrentHashMap<>());
457+
static void debug() {
458+
for(var exchange : allHttp2Exchanges) {
459+
exchange.http2.debug();
460+
}
461+
}
476462

477463
Http2Exchange(String protocol, HttpConnection conn) throws IOException {
478464
this.connection = conn;
479465
this.protocol = protocol;
480466

481-
http2 = new HTTP2Connection(conn, connection.getInputStream(), connection.getOutputStream(), this);
467+
if(protocol.equals("https2")) {
468+
http2Stats.sslConnections.incrementAndGet();
469+
} else {
470+
http2Stats.nonsslConnections.incrementAndGet();
471+
}
472+
473+
http2 = new HTTP2Connection(conn,http2Stats,connection.getInputStream(), connection.getOutputStream(), this);
482474
}
483475

484476
static TimerTask createTask() {
@@ -512,7 +504,8 @@ public void run() {
512504
http2.handle();
513505
} catch (HTTP2Exception ex) {
514506
logger.log(Level.WARNING, "ServerImpl http2 protocol exception "+http2,ex);
515-
} catch (EOFException | SocketException ex) {
507+
} catch (IOException ex) {
508+
stats.socketExceptionCount.incrementAndGet();
516509
logger.log(Level.DEBUG, "end of stream "+http2);
517510
} catch (Exception ex) {
518511
logger.log(Level.WARNING, "ServerImpl unexpected exception handling http2 connection "+http2, ex);
@@ -536,7 +529,9 @@ public Executor getExecutor() {
536529
@Override
537530
public void handleStream(HTTP2Stream stream,InputStream in, OutputStream out) throws IOException {
538531
connection.requestCount.incrementAndGet();
539-
requestCount.incrementAndGet();
532+
stats.requestCount.incrementAndGet();
533+
534+
http2Stats.totalStreams.incrementAndGet();
540535

541536
var request = stream.getRequestHeaders();
542537
var response = stream.getResponseHeaders();
@@ -584,18 +579,24 @@ public void handleStream(HTTP2Stream stream,InputStream in, OutputStream out) th
584579
return;
585580
}
586581

587-
logger.log(Level.DEBUG,() -> "http2 request on "+connection+" "+method+" for "+uri);
582+
logger.log(Level.TRACE,() -> "http2 request on "+connection+" "+method+" for "+uri);
588583

589584
final List<Filter> sf = ctx.getSystemFilters();
590585
final List<Filter> uf = ctx.getFilters();
591586

592587
final Filter.Chain sc = new Filter.Chain(sf, ctx.getHandler());
593588
final Filter.Chain uc = new Filter.Chain(uf, new LinkHandler(sc));
594589

595-
if (https) {
596-
uc.doFilter(new Http2ExchangeImpl(stream,uri,method,ctx,request,response,in,out));
597-
} else {
598-
uc.doFilter(new Http2ExchangeImpl(stream,uri,method,ctx,request,response,in,out));
590+
try {
591+
if (https) {
592+
uc.doFilter(new Http2ExchangeImpl(stream,uri,method,ctx,request,response,in,out));
593+
} else {
594+
uc.doFilter(new Http2ExchangeImpl(stream,uri,method,ctx,request,response,in,out));
595+
}
596+
} catch (IOException e) {
597+
} catch (Exception e) {
598+
logger.log(Level.WARNING, "Dispatcher Exception on "+stream, e);
599+
stats.handleExceptionCount.incrementAndGet();
599600
}
600601
}
601602
}
@@ -630,7 +631,7 @@ public void run() {
630631
} catch (SocketException e) {
631632
// these are common with clients breaking connections etc
632633
logger.log(Level.TRACE, "ServerImpl IOException", e);
633-
socketExceptionCount.incrementAndGet();
634+
stats.socketExceptionCount.incrementAndGet();
634635
closeConnection(connection);
635636
break;
636637
} catch (Exception e) {
@@ -675,14 +676,14 @@ private void runPerRequest() throws IOException {
675676

676677
connection.inRequest = true;
677678

678-
if (requestLine == null) {
679+
if (requestLine == null || "".equals(requestLine)) {
679680
/* connection closed */
680681
logger.log(Level.DEBUG, "no request line: closing");
681682
closeConnection(connection);
682683
return;
683684
}
684685
connection.requestCount.incrementAndGet();
685-
requestCount.incrementAndGet();
686+
stats.requestCount.incrementAndGet();
686687

687688
logger.log(Level.DEBUG, () -> "Exchange request line: "+ requestLine);
688689
int space = requestLine.indexOf(" ");
@@ -870,7 +871,7 @@ void sendReply(
870871
}
871872
} catch (IOException e) {
872873
logger.log(Level.TRACE, "ServerImpl.sendReply", e);
873-
replyErrorCount.incrementAndGet();
874+
stats.replyErrorCount.incrementAndGet();
874875
closeConnection(connection);
875876
}
876877
}
@@ -915,7 +916,7 @@ public void run() {
915916
for (var c : allConnections) {
916917
if (now- c.lastActivityTime >= IDLE_INTERVAL && !c.inRequest) {
917918
logger.log(Level.DEBUG, "closing idle connection");
918-
idleCloseCount.incrementAndGet();
919+
stats.idleCloseCount.incrementAndGet();
919920
closeConnection(c);
920921
// idle.add(c);
921922
} else if (c.noActivity && (now - c.lastActivityTime >= NEWLY_ACCEPTED_CONN_IDLE_INTERVAL)) {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package robaho.net.httpserver;
2+
3+
import java.util.concurrent.atomic.AtomicLong;
4+
5+
class ServerStats {
6+
final AtomicLong connectionCount = new AtomicLong();
7+
final AtomicLong requestCount = new AtomicLong();
8+
final AtomicLong handleExceptionCount = new AtomicLong();
9+
final AtomicLong socketExceptionCount = new AtomicLong();
10+
final AtomicLong idleCloseCount = new AtomicLong();
11+
final AtomicLong replyErrorCount = new AtomicLong();
12+
final AtomicLong maxConnectionsExceededCount = new AtomicLong();
13+
14+
private volatile long lastStatsTime = System.currentTimeMillis();
15+
16+
public String stats() {
17+
long now = System.currentTimeMillis();
18+
double secs = (now-lastStatsTime)/1000.0;
19+
lastStatsTime = now;
20+
21+
long _requests = requestCount.getAndSet(0);
22+
23+
return
24+
"Connections Since: "+connectionCount.getAndSet(0)+"\n" +
25+
"Requests Since: "+_requests+"\n" +
26+
"Requests/sec: "+(long)(_requests/secs)+"\n"+
27+
"Total Handler Exceptions: "+handleExceptionCount.get()+"\n"+
28+
"Total Socket Exceptions: "+socketExceptionCount.get()+"\n"+
29+
"Total Max Connections Exceeded: "+maxConnectionsExceededCount.get()+"\n"+
30+
"Total Idle Closes: "+idleCloseCount.get()+"\n"+
31+
"Total Reply Errors: "+replyErrorCount.get()+"\n";
32+
}
33+
}

0 commit comments

Comments
 (0)