Memra

Worked build: access logging, end to end

◈ 6 cards

Capture the client address at accept, the request line after parsing, the status and byte count after responding — then emit one common-log-format record per request and verify it with your Assignment 1 analyser.

Two logs, and only one of them is about clients

A server keeps an audit log and an error log, and the split is not cosmetic. The audit log gets one entry per request — including every 400, 404 and 501, because a client sending a bad request is ordinary traffic, not a malfunction. The error log gets unexpected exceptions only, and its ideal length is zero lines: every entry is a bug you have not fixed yet. If you investigate one and conclude the code worked as intended, delete the log statement rather than learn to ignore it.

java.util.logging covers this with no dependencies. Create one Logger per purpose as a private static final field; Logger is thread-safe, which it must be, since even distinct loggers usually share a file. Of its seven levels — SEVERE > WARNING > INFO > CONFIG > FINE > FINER > FINEST — use INFO for audit records and WARNING/SEVERE for errors. Never ship debug logging and never log "just in case": when a real problem arrives you will be hunting the one line that matters inside everything you logged speculatively.

One record, written once, at the end

The fields become known at three different moments. The client address exists the instant accept() returns. The request line exists after you parse. The status and the byte count only exist after you have answered. So the record can only be written at the end — and exactly once, whatever happened in between.

That is what a finally block is for. Initialise status = 500 and bytes = 0 before the try, overwrite them on each successful path, and log in finally. A handler that dies on an unexpected exception then logs a 500 automatically, which is both true and the only way that request appears in the audit log at all.

The record Assignment 1 has to be able to read

A2 asks for the same format your A1 program parses, and A1 takes everything before the first space as the client address and passes the rest through untouched. That fixes the layout: address ident authuser [timestamp] "request line" status bytes, with - standing in for the two identity fields nobody fills in any more.

Two details decide whether the pair works. Field one must be a bare numeric address: connection.getInetAddress().getHostAddress() gives 203.0.113.7, while getRemoteSocketAddress().toString() gives /203.0.113.7:54321 — a leading slash and an ephemeral port, both of which choke a parser that tries to resolve field one. And the timestamp must pin Locale.US in its DateTimeFormatter, because MMM renders the month name in the JVM’s default locale, and a log full of août will not parse against a pattern expecting Aug.

Why one shared Logger settles the synchronisation question

Handlers run concurrently on a pool and all write to one file — the interleaved-log problem from the concurrency module, in the place it actually bites. The answer is not a lock you write. It is to build the whole record as one string and pass it to one info() call: Logger is thread-safe, so one call is one record. Three calls for three fields would reintroduce the problem immediately, because the thread safety is per call, not per handler.

Worked example — instrumenting the handler

public class RequestHandler implements Runnable {

    private static final Logger requests = Logger.getLogger("requests");
    private static final Logger errors   = Logger.getLogger("errors");
    private static final DateTimeFormatter STAMP =
            DateTimeFormatter.ofPattern("dd/MMM/yyyy:HH:mm:ss Z", Locale.US);

    private final Socket connection;
    private final Path root;

    @Override
    public void run() {
        String client = connection.getInetAddress().getHostAddress();
        String requestLine = "";
        int status = 500;
        long bytes = 0;
        try (Socket socket = connection) {
            requestLine = readRequestLine(socket);       // field 5
            Result r = handle(requestLine, socket);      // fields 6 and 7
            status = r.status();
            bytes  = r.bytes();
        } catch (IOException ex) {
            errors.log(Level.WARNING, "failed talking to " + client, ex);
        } finally {
            requests.info(String.format("%s - - [%s] \"%s\" %d %d",
                    client, STAMP.format(ZonedDateTime.now()), requestLine, status, bytes));
        }
    }
}

One record per request, on every path. A produced line looks like this:

203.0.113.7 - - [03/Aug/2026:14:02:11 -0600] "GET /docs/index.html HTTP/1.1" 200 4096

Verifying the pair

A2 asks you to document this step, and it is the only proof that the two programs agree. Start the server, drive it with a browser (which fetches the page and every image on it, so one visit yields several records), then with telnet for a deliberate 404 and a deliberate 501. Stop the server and run your A1 analyser over the log it just wrote.

Check three things: every line parses, the per-host access count matches the requests you made, and the byte totals match the file sizes. A mismatch is almost always a slash and a port in field one, a locale-dependent month, or a status logged before it was known. Keep the transcript — it is the test plan the assignment asks for.

fieldwhat the server writeswhat the analyser does1 client address203.0.113.7resolves it to a hostname2 ident-passes it through3 authuser-passes it through4 timestamp[03/Aug/2026:14:02:11-0600]passes it through5 request line"GET /docs/index.htmlHTTP/1.1"passes it through6 status200tallies accesses per host7 body bytes4096sums bytes per hostField 1 must be bare: no leading slash, no port.
The format is a contract between two programs you wrote. Field one is the one that must be exact: the analyser takes everything before the first space as an address and resolves it, so a leading slash or a trailing port breaks every line in the file.
acceptclient addressparserequest linerespondstatus + byteslog onceone info() callIn finally, so a thrown exception still logs its 500.
Three of the seven fields do not exist until the response has been written, which is what forces the log call into a finally block rather than anywhere earlier that might feel more natural.
NORMAL ~/memra/learn/comp-348/http-server-access-logging utf-8 LF