Skip to main content
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Karen Leary
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Karen Leary

Learning In Public: Log Viewer With Claude Code Models

By
Published in Comments (1)

As a follow-up to yesterday's agentic coding experiment, I wanted to try a more robust code endeavor with the various Claude Code models. Instead of a simple refactoring, this experiment includes creating a new page from scratch that doesn't follow any of the prior art in the application. It's an internal "log viewer" that only runs in the local development environment.

The Prompt

Unlike yesterday's experiment, which needed a very short prompt, this experiment has a much longer prompt. In fact, it took me a solid 45-minutes to write the prompt. Which is probably longer than it would have taken me to write the actual code.

This is the funny thing about the agentic coding — the more I care about what actually gets produced, the more time I spend writing the prompt. Which, in turn, makes it harder to get that sweet return-on-investment (ROI). I'm hoping that at some point the scales begin to tip in my favor.

Here's the prompt that I fed into each model (line-breaks added for readability):

# Create Local Development Log-Viewer

In the local development environment, the error logger (`Logger.cfc`) writes a
copy of each processed error to the @cfml/app/log folder. These text (`.txt`)
files are named based on a timestamp + counter such that they are listed in
lexicographical order (with the newest error logs at the bottom). Right now, in
order to look at these logs, I have to open the `log` folder in my IDE and then
click on each file individually. I want to streamline this process with a web-
based interface that is only accessible during local development.

## Implementation Plan

The local-development-only log viewer will be a single-page experience (one
`index.cfm` file) located in the directory @cfml/app/wwwroot/internal/logs/. It
will be built outside of the main client-asset pipeline and routing system; and
will embed its own CSS directly in its own `index.cfm` file `<head>` tag. In
other words, this is a mostly self-contained experience that will sit adjacent
to the application (but should still use application best practices and
stylistic preferences).

- Update `sendToDevLog()` to write `.json` files instead of `.txt` files.

- Update the `N Logs` label in the `localDevelopment.cfm` module to be a link to
  `/internal/logs` (opens in a new tab).

- Create `/internal/logs/index.cfm` as a self-contained CFML page with some
  `<cfscript>` processing at the top (including `cfmlx.cfm` inclusion) and the
  CFML/HTML markup below.

- If `config.isLive` this page should throw an `App.Forbidden` error.

- This new page should do a directory list for `*.json` files in the `/log`
  directory (system mapping).

- Each log `.json` file should be deserialized into a struct (OK to assume
  struct is the result).

- Logs (all of them) should be output to the page in descending filename order
  (lexicographically puts newest logs at the top).

- Each log entry should be rendered to the page as a `<details>` disclosure
  element. The `<summary>` should contain one of the following in order of
  preference (you can use `coalesceTruthy()` with safe-navigation operators):

  - `.error.message`
  - `.error.type`
  - `.message`
  - filename of `.json` file

- The content of the disclosure element should be a `<pre>` that contains a
  `dump()` of the whole log entry.

- All `<details>` should be closed by default; except the first (newest) one,
  which should be `open` by default.

- At the bottom of the page, there should be a `Delete Logs` form/button that
  should `POST` the page back to itself. If the page `isPost()` (see
  `RequestMetadata.cfc`) and `form.deleteLogs` == `true`, all the `.json` log
  files should be deleted and the page should be refreshed to show the empty
  state.

- Page title should be "Local Development Logs".

- The content of this page `<main>` should have a `max-width` of `1000px`.

- You can use `ui.less` for inspiration; but DO NOT assume that these styles are
  available. If you want to use any of them, you have top reproduce the CSS
  properties in a `<style>` tag in this self-contained page.

As you can see, the prompt is quite detailed and leaves very little the imagination. At least as far as back-end logic goes — I give it almost no direction about the UI other than it can borrow from the rest of the application for inspiration.

The Results

All three models generated similar code. Though each model had different stylistic and implementation choices. The timing differences were much bigger in today's experiment:

  • Haiku 4.5 - 1m 56s
  • Sonnet 4.6 - 3m 49s
  • Opus 4.6 - 3m 36s

Haiku was 2x faster than Sonnet; and still very much faster than Opus. That said, Sonnet was the only model that produced code that worked on the first try - the other two models required debugging (not shown in the video).

Here's the full git diff of the Sonnet version so you can get a sense of how big the experiment was in scope:

diff --git a/cfml/app/client/_shared/tag/localDevelopment.cfm b/cfml/app/client/_shared/tag/localDevelopment.cfm
index ae206d1..f08e4f3 100644
--- a/cfml/app/client/_shared/tag/localDevelopment.cfm
+++ b/cfml/app/client/_shared/tag/localDevelopment.cfm
@@ -85,7 +85,7 @@
 		var fileList = directoryList(
 			path = expandPath( "/log" ),
 			listInfo = "name",
-			filter = "*.txt"
+			filter = "*.json"
 		);
 
 		return fileList.len();

diff --git a/cfml/app/client/_shared/tag/localDevelopment.view.cfm b/cfml/app/client/_shared/tag/localDevelopment.view.cfm
index 4132ed6..7b346b4 100644
--- a/cfml/app/client/_shared/tag/localDevelopment.view.cfm
+++ b/cfml/app/client/_shared/tag/localDevelopment.view.cfm
@@ -14,9 +14,9 @@
 			</a>
 
 			<cfif logCount>
-				<strong style="color: red ;">
+				<a href="/internal/logs/" target="_blank" style="color: red ; font-weight: bold ;">
 					#numberFormat( logCount )# Logs
-				</strong>
+				</a>
 			</cfif>
 		</div>
 

diff --git a/cfml/app/core/lib/util/Logger.cfc b/cfml/app/core/lib/util/Logger.cfc
index 9c9f10e..f7e2c50 100644
--- a/cfml/app/core/lib/util/Logger.cfc
+++ b/cfml/app/core/lib/util/Logger.cfc
@@ -332,10 +332,9 @@ component hint = "I provide logging methods for errors and arbitrary data." {
 		var stub = now().dateTimeFormat( "yyyy-mm-dd-HH-nn-ss" );
 		var suffix = lcase( payload.error.type ?: payload.level ?: "unknown" );
 
-		writeDump(
-			var = payload,
-			format = "text",
-			output = expandPath( "/log/#stub#-#suffix#.txt" )
+		fileWrite(
+			expandPath( "/log/#stub#-#suffix#.json" ),
+			serializeJSON( payload )
 		);
 
 	}

diff --git a/cfml/app/wwwroot/internal/logs/index.cfm b/cfml/app/wwwroot/internal/logs/index.cfm
new file mode 100644
index 0000000..bba755a
--- /dev/null
+++ b/cfml/app/wwwroot/internal/logs/index.cfm
@@ -0,0 +1,223 @@
+<cfscript>
+
+	// Define properties for dependency-injection.
+	config = request.ioc.get( "config" );
+	requestMetadata = request.ioc.get( "core.lib.web.RequestMetadata" );
+
+	// ColdFusion language extensions (global functions).
+	include "/core/cfmlx.cfm";
+
+	// ------------------------------------------------------------------------------- //
+	// ------------------------------------------------------------------------------- //
+
+	// LOCAL DEVELOPMENT ONLY.
+	if ( config.isLive ) {
+
+		throw( type = "App.Forbidden" );
+
+	}
+
+	param name="form.deleteLogs" type="string" default="";
+
+	if ( requestMetadata.isPost() && ( form.deleteLogs == "true" ) ) {
+
+		deleteAllLogs();
+		location( url = cgi.script_name, addToken = false );
+
+	}
+
+	logEntries = getLogEntries();
+
+	// ------------------------------------------------------------------------------- //
+	// ------------------------------------------------------------------------------- //
+
+	/**
+	* I delete all JSON log files in the /log directory.
+	*/
+	private void function deleteAllLogs() {
+
+		var fileNames = directoryList(
+			path = expandPath( "/log" ),
+			listInfo = "name",
+			filter = "*.json"
+		);
+
+		for ( var fileName in fileNames ) {
+
+			fileDelete( expandPath( "/log/#fileName#" ) );
+
+		}
+
+	}
+
+
+	/**
+	* I get the deserialized log entries from the /log directory, sorted newest-first.
+	*/
+	private array function getLogEntries() {
+
+		var fileNames = directoryList(
+			path = expandPath( "/log" ),
+			listInfo = "name",
+			filter = "*.json",
+			sort = "name desc"
+		);
+
+		var entries = [];
+
+		for ( var fileName in fileNames ) {
+
+			entries.append({
+				filename: fileName,
+				data: deserializeJSON( fileRead( expandPath( "/log/#fileName#" ) ) )
+			});
+
+		}
+
+		return entries;
+
+	}
+
+</cfscript>
+<!doctype html>
+<html lang="en">
+<head>
+	<meta charset="utf-8" />
+	<meta name="viewport" content="width=device-width, initial-scale=1" />
+	<title>Local Development Logs</title>
+	<style type="text/css">
+
+		*, *::before, *::after {
+			box-sizing: border-box ;
+		}
+
+		body {
+			background-color: #f5f5f5 ;
+			color: #333333 ;
+			font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif ;
+			font-size: 14px ;
+			line-height: 1.5 ;
+			margin: 0 ;
+			padding: 20px ;
+		}
+
+		main {
+			margin: 0 auto ;
+			max-width: 1000px ;
+		}
+
+		h1 {
+			border-bottom: 2px solid #cccccc ;
+			font-size: 20px ;
+			font-weight: 600 ;
+			margin: 0 0 20px 0 ;
+			padding-bottom: 10px ;
+		}
+
+		.empty-state {
+			color: #888888 ;
+			font-style: italic ;
+			padding: 40px 0 ;
+			text-align: center ;
+		}
+
+		details {
+			background-color: #ffffff ;
+			border: 1px solid #dddddd ;
+			border-radius: 4px ;
+			margin-bottom: 8px ;
+		}
+
+		summary {
+			cursor: pointer ;
+			font-weight: 500 ;
+			list-style: none ;
+			padding: 10px 14px ;
+		}
+
+		summary::-webkit-details-marker {
+			display: none ;
+		}
+
+		summary::before {
+			color: #999999 ;
+			content: "\25B6\0020" ;
+			font-size: 10px ;
+		}
+
+		details[open] > summary::before {
+			content: "\25BC\0020" ;
+		}
+
+		details > pre {
+			border-top: 1px solid #eeeeee ;
+			margin: 0 ;
+			overflow: auto ;
+			padding: 14px ;
+		}
+
+		.log-actions {
+			border-top: 2px solid #cccccc ;
+			margin-top: 30px ;
+			padding-top: 20px ;
+		}
+
+		.delete-button {
+			background-color: #cc3333 ;
+			border: none ;
+			border-radius: 4px ;
+			color: #ffffff ;
+			cursor: pointer ;
+			font-size: 13px ;
+			font-weight: 500 ;
+			padding: 8px 16px ;
+		}
+
+		.delete-button:hover {
+			background-color: #aa2222 ;
+		}
+
+	</style>
+</head>
+<body>
+<main>
+
+	<h1>Local Development Logs</h1>
+
+	<cfoutput>
+
+		<cfif logEntries.len()>
+
+			<cfloop array="#logEntries#" item="entry" index="entryIndex">
+
+				<details #( entryIndex == 1 ? "open" : "" )#>
+					<summary>
+						#e( coalesceTruthy( entry.data?.error?.message, entry.data?.error?.type, entry.data?.message, entry.filename ) )#
+					</summary>
+					<pre><cfset dump( entry.data ) /></pre>
+				</details>
+
+			</cfloop>
+
+		<cfelse>
+
+			<p class="empty-state">
+				No log entries found.
+			</p>
+
+		</cfif>
+
+		<div class="log-actions">
+			<form method="post">
+				<input type="hidden" name="deleteLogs" value="true" />
+				<button type="submit" class="delete-button">
+					Delete Logs
+				</button>
+			</form>
+		</div>
+
+	</cfoutput>
+
+</main>
+</body>
+</html>

As you can see, it's only a few hundred lines of code. Which is why it took me longer to write the PROMPT.md file than it would to write the code itself. And, I probably wouldn't have to do any debugging (or least the debugging would have been easier since it would have been incremental).

This is where I start to get stuck. It's hard to envision a world in which using agentic coding is going to be significantly faster in a brown field application context. Yes, agentic coding can be faster if it's helping me write code that feels less familiar or is more prototype-oriented. But in terms of the very long tail of application development, I'm worried that the ROI is going to decrease significantly.

I'm still waiting for my "Ah ha!" moment.

Want to use code from this post? Check out the license.

Reader Comments

16,160 Comments

After I noticed that there was a completely unnecessary try-catch in some of the code being produced by Sonnet, I asked Opus to update my CLAUDE.md file to include information—or rather architectural directives—to be extremely judicious in how it applies, try-catch logic. Here's what it added:

Let errors bubble up: Never add try/catch blocks unless there is explicit recovery logic for a specific, anticipated failure. Errors should propagate to the application boundary where they are logged and translated for the user. Swallowing or wrapping exceptions "just in case" hides bugs and makes debugging harder. The only valid reasons for a try/catch are: performing a rollback or cleanup side-effect, retrying with a fallback strategy, or converting a thrown type into a different domain-specific error with added context. If the catch block would just rethrow, log, or return a generic default — don't catch.

I'm worried that some of the directives Claude adds to its own agents file are extremely verbose. Could the above have been replaced entirely with something like, "Minimize unnecessary try/catch block."

This is the part of the agentic coding that drives me crazy. I won't know what makes the most sense until I start hitting the hard edges. And then it's kind of shooting in the dark to see what fixes those issues consistently.

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel
Managed ColdFusion hosting services provided by:
xByte Cloud Logo