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:
- A grid view of all available Micrometer metrics with their current values.
- Clickable metrics that open a modal with detailed information.
- Automatic refresh every 30 seconds.
Grid View
Detail View
Final Results
- "Make the grid view more compact"
- "Add search to the grid view"
- "Have the details screen return HTML instead of JSON"
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 codepackage 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">×</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
Post a Comment