Activity tracking and timings¶
Follow connections, exchanges, tunnels and flows from Java with ActivityTracker callbacks: where
each response came from, per-exchange timings, failures, TLS handshakes and one record per exchange
or flow. Use this page to build your own logging or metrics; the access log,
metrics and OpenTelemetry are built on the same data.
ActivityTracker callbacks¶
ActivityTracker callbacks run on the connection's virtual thread. Besides LittleProxy's events:
- Where a response came from:
responseSentToClient(ctx, response, source)receives aResponseSource:SERVER(relayed, maybe with headers or body edited in place),PROXY(the proxy's error answers,407, theCONNECT200),FILTER(a short-circuit, a failure answer from a filter, or a server or proxy response a filter replaced or gave another status),CACHEorREPLAY(a recorded response fromServerReplay).ctx.upstreamStatus()is the status the server sent, so a500a filter turned into a200still shows. The two-argument callback keeps working. - Timings:
ctx.timings()returns aFlowTimingssnapshot for the exchange in progress:dnsLookup(),connect(),tlsHandshake()(towards the server or a TLS chained proxy),clientTlsHandshake(),timeToFirstByte()andtotal(), plus the raw offsets from the first byte of the request. A request on a reused connection has no lookup or connect; when intercepting, the server handshake belongs to theCONNECTexchange. Take the snapshot inresponseCompleted(ctx, response), which follows the last byte of the response. Recording costs a fewSystem.nanoTime()calls and no allocation per request. - Server-side failures:
serverConnectionExceptionCaught(serverContext, cause)reports each failed connection attempt, server timeout or bad response once, with the server (or chained proxy) it concerned. Client-side errors still go toconnectionExceptionCaught. - TLS handshakes:
tlsHandshakeFailed(ctx, clientSide, cause)reports each failed or timed-out handshake: with a client (TLS listener or interception) or with a server or TLS chained proxy (thenctxis aFullFlowContextnaming it). - Correlation:
ctx.getConnectionId()andctx.acceptedAt()identify the client connection, and the proxy's log lines about it start with[conn <id>]. - HTTP/2 streams:
ctx.getStreamId()names the stream (0 for HTTP/1), and log lines about it start with[conn <id> stream <id>]. Each stream has its own context, with its own timings and upstream status; contexts are equal only for the same connection and stream, andctx.getConnectionContext()is the connection's. Callbacks for one connection's streams run concurrently, on the streams' threads. A stream that ends without a complete response (the client reset it, or the connection closed) is reported withconnectionExceptionCaught(streamCtx, cause). The request'sprotocolVersion()isHTTP/2.0; access logs sayHTTP/2, as Envoy does. - Transparent QUIC: each flow of the transparent QUIC listener is a
client connection of its own (
clientConnected...clientDisconnected, withctx.isTransparentQuic()true), whose made-upCONNECTis reported as a request.transparentQuicFlowDecided(ctx, clientHello, intercepted, reason)says whether it is intercepted or relayed, and why; a relayed flow then reportsserverConnected, the bytes both ways andserverDisconnected. The loggerorg.microproxy.impl.TransparentQuicListenerwrites each decision, and why a new client was dropped, at DEBUG. - Why an exchange ended as it did:
ctx.failure()(theProxyFailurethe proxy answered, set before filters see it),ctx.responseFlags()(Envoy's flags:UF,DC,UT, ...) andctx.responseCodeDetails()(via_upstream,proxy_failure:connect_failed, ...); see Response flags and details. Filters may add their own withctx.addResponseFlag(flag)andctx.setResponseCodeDetails(details). - One record per exchange or flow:
flowEnded(record)comes once per HTTP exchange, when it has ended however it ended, and once per tunnel (CONNECT, SOCKS, WebSocket, TCP hooks), CONNECT-UDP flow and relayed or stream-intercepted QUIC flow when it closes. TheFlowRecordhas the request and response heads, the bytes each way (to the client and on the server's wire), the upstream and local addresses and TLS sessions, the duration, the flags and details, and what kind of flow it was; the access log is made of them. - In filters:
HttpFilters.proxyToClientResponseSent(response, source)is called once a response has been written in full, whoever made it, with the head as sent and itsResponseSource;ctx.timings()covers the whole exchange by then.HttpFilters.exchangeEnded(completed)follows exactly once however the exchange ended (also when the client left half-way, the server failed or a filter aborted; for tunnels when they close), so filters can release what they hold per exchange.
TLS handshake log lines¶
The logger org.microproxy.impl.Tls writes one line per handshake event, without stack traces:
| event | level | contents |
|---|---|---|
| started | DEBUG | peer address, host, the proxy's TLS role (mode=client / server), whether a client certificate is required |
| succeeded | DEBUG | the same, the duration, negotiated protocol and cipher suite, the ALPN protocol (alpn=h2) when one was negotiated, and the SNI name a client asked for |
| failed with a client | DEBUG | the error and its root cause (certificate_unknown, no cipher suites in common, TLS handshake not finished within 10000 ms, ...), and a certificate summary: the proxy's own certificate, then the peer's chain (subject, issuer, notAfter, SANs) |
| failed with a server or chained proxy | WARNING for certificate problems (untrusted, expired, wrong name, client certificate refused), DEBUG when the server does not speak TLS (the CONNECT is tunnelled instead), INFO otherwise (timeouts, protocol mismatches) |
the same |
Client handshakes fail routinely (clients giving up, scanners, clients that do not trust the
interception CA), hence DEBUG. The JDK discards a peer chain it rejected for an unknown issuer, so
for that failure the summary has no peer chain. Turn the lines on with, for example,
-Djava.util.logging.config.file=... setting org.microproxy.impl.Tls.level = FINE.