Using Claude AI to generate code to view Spring Boot 3/Micrometer metrics

Introduction

In this blog post, we'll explore how AI, specifically Claude AI, can assist developers in quickly generating code solutions. We'll use the example of creating a readable Micrometer metrics dashboard for a Spring Boot 3 application. 

(Note: Though Claude was used this should be able to be accomplished on any LLM such as Gemini, ChatGPT, Bing CoPilot, ...)

The Problem

Spring Boot applications using Micrometer provide metrics through the /actuator/metrics endpoint, but this data is not easily readable for humans during development and testing. We needed a user-friendly way to view these metrics without additional dependencies.

The Claude Prompt

To solve this problem, we can use Claude by providing a specific prompt. Here's the prompt we used:

Can you create a Java REST controller for Spring Boot 3 that serves an HTML page with embedded JavaScript to display Micrometer metrics in a readable format? The page should show all metrics with their values and allow clicking on a metric to see its details. The HTML should be returned as a string from the controller method.

This prompt clearly outlines our requirements: a Spring Boot controller serving an HTML page with JavaScript to display metrics in a readable format, including the ability to view details.

Note: While embedding HTML and JavaScript directly in a RestController is not a best practice for production applications, this approach serves as a concise demonstration of the concept. In a real-world scenario, it's advisable to separate concerns by using proper templating engines or serving static files.

Initial Results

When implemented, this solution provides:

  1. A grid view of all available Micrometer metrics with their current values.
  2. Clickable metrics that open a modal with detailed information.
  3. Automatic refresh every 30 seconds.
Snapshots of the grid view and detailed view follow...

Grid View



Detail View




Final Results

The process was iterative.  After reviewing each version we would prompt Claude to make improvements such as...
  • "Make the grid view more compact"
  • "Add search to the grid view"
  • "Have the details screen return HTML instead of JSON"
Note Iwenever had to modify the code by hand in any of the iterations that we tested.  It was our job to define the requirements.

The final results from this iterative process follow (Final Code) ...

Grid View



Detail View



The Generated Code

Based on this prompt, Claude generated a comprehensive solution. Here's the Java controller it produced (note this is the Final Version after I specified further requirements in a Claude session). To view the code on github: Final Code

java code
package com.stevesouza.opentelemetry;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; /** * Code generated by claude AI and modified */ @RestController public class MetricsDashboardController { @GetMapping("/metrics") @ResponseBody public String getMetricsDashboard() { return """ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Metrics Dashboard</title> <style> body { font-family: Arial, sans-serif; margin: 0; padding: 10px; background-color: #f0f0f0; } h1 { color: #333; margin-bottom: 10px; } #searchInput { width: 100%; padding: 5px; margin-bottom: 10px; box-sizing: border-box; font-size: 14px; } .metric-group { margin-bottom: 10px; background-color: white; border-radius: 4px; padding: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.12); } .group-header { font-weight: bold; font-size: 14px; margin-bottom: 3px; color: #0066cc; } .metrics-container { display: flex; flex-wrap: wrap; gap: 4px; } .metric { background-color: #f9f9f9; border-radius: 4px; padding: 4px 6px; font-size: 12px; cursor: pointer; } .metric:hover { background-color: #e9e9e9; } .metric-name { color: #333; } .metric-value { color: #009900; font-weight: bold; } #detailsModal { display: none; position: fixed; z-index: 1; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.4); } .modal-content { background-color: #fefefe; margin: 15% auto; padding: 20px; border: 1px solid #888; width: 80%; max-width: 600px; } .close { color: #aaa; float: right; font-size: 28px; font-weight: bold; } .close:hover, .close:focus { color: black; text-decoration: none; cursor: pointer; } .section { margin-top: 10px; font-size: 14px; } .header { font-weight: bold; display: inline; margin-right: 5px; } .tag { background: #eee; padding: 2px 5px; margin: 2px; display: inline-block; } </style> </head> <body> <h1>Metrics Dashboard</h1> <input type="text" id="searchInput" placeholder="Search metrics..."> <div id="metrics"></div> <div id="detailsModal"> <div class="modal-content"> <span class="close">&times;</span> <h2 id="modalTitle"></h2> <div id="modalContent"></div> </div> </div> <script> let allMetrics = []; async function fetchMetricValue(metricName) { try { const response = await fetch(`/actuator/metrics/${metricName}`); const data = await response.json(); return { value: data.measurements[0]?.value ?? 'N/A', details: data }; } catch (error) { console.error(`Error fetching ${metricName}:`, error); return { value: 'Error', details: null }; } } function groupMetrics(metrics) { const groups = {}; metrics.forEach(metric => { const groupName = metric.split('.')[0]; if (!groups[groupName]) { groups[groupName] = []; } groups[groupName].push(metric); }); return groups; } async function updateDashboard(filter = '') { try { if (allMetrics.length === 0) { const response = await fetch('/actuator/metrics'); const data = await response.json(); allMetrics = data.names; } const filteredMetrics = allMetrics.filter(metric => metric.toLowerCase().includes(filter.toLowerCase()) ); const groupedMetrics = groupMetrics(filteredMetrics); const sortedGroups = Object.keys(groupedMetrics).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()) ); const metricsDiv = document.getElementById('metrics'); metricsDiv.innerHTML = ''; for (const group of sortedGroups) { const groupDiv = document.createElement('div'); groupDiv.className = 'metric-group'; const groupHeader = document.createElement('div'); groupHeader.className = 'group-header'; groupHeader.textContent = `${group}:`; groupDiv.appendChild(groupHeader); const metricsContainer = document.createElement('div'); metricsContainer.className = 'metrics-container'; const sortedMetrics = groupedMetrics[group].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()) ); for (const metricName of sortedMetrics) { const { value, details } = await fetchMetricValue(metricName); const metricDiv = document.createElement('div'); metricDiv.className = 'metric'; metricDiv.innerHTML = `<span class="metric-name">${metricName}:</span> <span class="metric-value">${value}</span>`; metricDiv.onclick = () => showDetails(metricName, details); metricsContainer.appendChild(metricDiv); } groupDiv.appendChild(metricsContainer); metricsDiv.appendChild(groupDiv); } } catch (error) { console.error('Error updating dashboard:', error); } } function showDetails(metricName, details) { const modal = document.getElementById('detailsModal'); const modalTitle = document.getElementById('modalTitle'); const modalContent = document.getElementById('modalContent'); modalTitle.textContent = metricName; let content = ` <div class="section"> <span class="header">Base Unit:</span> ${details.baseUnit || 'N/A'} </div> <div class="section"> <span class="header">Measurements:</span> ${details.measurements.map(m => `${m.statistic}: ${m.value}`).join(', ')} </div> `; if (details.availableTags && details.availableTags.length > 0) { content += ` <div class="section"> <span class="header">Available Tags:</span> ${details.availableTags.map(tag => `<span class="tag">${tag.tag}: ${tag.values.join(', ')}</span>`).join(' ')} </div> `; } modalContent.innerHTML = content; modal.style.display = 'block'; } // Close modal when clicking on 'x' document.querySelector('.close').onclick = function() { document.getElementById('detailsModal').style.display = 'none'; } // Close modal when clicking outside of it window.onclick = function(event) { const modal = document.getElementById('detailsModal'); if (event.target == modal) { modal.style.display = 'none'; } } // Add event listener for search input document.getElementById('searchInput').addEventListener('input', (e) => { updateDashboard(e.target.value); }); updateDashboard(); setInterval(() => updateDashboard(document.getElementById('searchInput').value), 30000); </script> </body> </html> """; } 

}

This controller serves an HTML page with embedded CSS and JavaScript, creating an interactive dashboard that displays all Micrometer metrics and allows users to view detailed information for each metric.

Conclusion

This example demonstrates how AI tools like Claude can rapidly generate functional code based on  well-crafted prompts. By leveraging AI, developers can quickly prototype solutions, learn new approaches, and accelerate their development process.

Remember to review and test AI-generated code thoroughly before using it in production environments.

Comments