Skip to content

Instantly share code, notes, and snippets.

@kchaitanya863
Created April 21, 2025 08:16
Show Gist options
  • Save kchaitanya863/ba7292d56a089fc47e6f906b4e58c4b5 to your computer and use it in GitHub Desktop.
Save kchaitanya863/ba7292d56a089fc47e6f906b4e58c4b5 to your computer and use it in GitHub Desktop.
Standalone Azure Application Insights Query & Visualization Tool
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>App Insights Query & Visualize Tool</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"/>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3/dist/chartjs-adapter-date-fns.bundle.min.js"></script> <style>
body { padding: 1em; }
.container { max-width: 1200px; margin: auto; } /* Wider container */
hgroup { margin-bottom: 1.5em; }
section { margin-bottom: 2em; padding-bottom: 1em; border-bottom: 1px solid var(--pico-border); }
section:last-of-type { border-bottom: none; }
#results-output article { padding: 1em; }
table { width: 100%; font-size: 0.9em; }
th, td { padding: 0.5em 0.75em; white-space: nowrap; } /* Prevent wrapping */
.error-message, .success-message { padding: 0.5em; border: 1px solid; border-radius: var(--pico-border-radius); margin-top: 0.5em; }
.error-message { color: var(--pico-color-red-600); background-color: var(--pico-color-red-100); border-color: var(--pico-color-red-300); }
.success-message { color: var(--pico-color-green-700); background-color: var(--pico-color-green-100); border-color: var(--pico-color-green-300); }
.warning-message { color: var(--pico-color-amber-700); background-color: var(--pico-color-amber-100); padding: 1em; border: 1px solid var(--pico-color-amber-300); border-radius: var(--pico-border-radius); margin-bottom: 1em; font-weight: bold; }
textarea { min-height: 150px; font-family: monospace; }
#chart-container { position: relative; min-height: 300px; max-height: 500px; width: 100%; } /* Relative for Chart.js */
#visualization-options { display: none; margin-top: 1em; padding: 1em; border: 1px dashed var(--pico-secondary-border); border-radius: var(--pico-border-radius); }
#visualization-options .grid label { margin-bottom: 0; } /* Tighter grid */
#stored-results-list ul { list-style: none; padding-left: 0; }
#stored-results-list li { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5em; padding: 0.5em; background-color: var(--pico-card-background-color); border: 1px solid var(--pico-card-border-color); border-radius: var(--pico-border-radius); }
#stored-results-list li span { flex-grow: 1; margin-right: 1em; font-size: 0.9em; }
#stored-results-list li button { font-size: 0.8em; padding: 0.2em 0.5em; margin-left: 0.5em; background-color: var(--pico-color-red-500); border-color: var(--pico-color-red-500); color: white; }
.loading-indicator { display: inline-block; margin-left: 10px; } /* Simple text loading */
small { color: var(--pico-secondary); }
</style>
</head>
<body>
<main class="container">
<hgroup>
<h1>App Insights Query & Visualize Tool</h1>
<p>Run KQL queries, store results locally, and create visualizations.</p>
</hgroup>
<article id="security-warning" class="warning-message">
<strong>Security Warning:</strong> Your Application Insights API Key will be stored in your browser's Local Storage and sent directly from the browser. This is insecure and exposes the key. Use only for non-sensitive data or personal testing where you accept the risk. Do NOT use this in shared environments.
</article>
<section id="connections-section">
<details> <summary><h2>Manage Connections</h2></summary>
<form id="connection-form">
<div class="grid">
<label for="conn-name"> Connection Name <input type="text" id="conn-name" name="conn-name" placeholder="My App Prod" required> </label>
<label for="conn-appid"> App ID (GUID) <input type="text" id="conn-appid" name="conn-appid" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" required> </label>
</div>
<label for="conn-apikey"> API Key <input type="password" id="conn-apikey" name="conn-apikey" placeholder="Your App Insights API Key" required> <small>Warning: Stored locally in browser!</small> </label>
<button type="submit">Save Connection</button>
</form>
<div id="connections-list" style="margin-top: 1em;">
<h3>Saved Connections:</h3>
<ul id="connections-ul"></ul>
<p id="no-connections-msg">No connections saved yet.</p>
</div>
</details>
</section>
<section id="query-section">
<h2>Run Query & Store Result</h2>
<form id="query-form">
<div class="grid">
<label for="query-connection">Select Connection
<select id="query-connection" name="query-connection" required>
<option value="" disabled selected>-- Select --</option>
</select>
</label>
<label for="query-save-name">Save Result As (Optional)
<input type="text" id="query-save-name" name="query-save-name" placeholder="e.g., Daily Errors Last 7d">
</label>
</div>
<label for="query-kql">KQL Query</label>
<textarea id="query-kql" name="query-kql" placeholder="requests | take 10" required></textarea>
<button type="submit" id="run-query-btn" aria-busy="false">Run Query</button>
<span id="query-status"></span>
</form>
</section>
<section id="stored-results-section">
<h2>Stored Query Results</h2>
<div id="stored-results-list">
<ul id="results-ul"></ul>
<p id="no-results-msg">No results saved yet.</p>
</div>
<button id="clear-results-btn" class="secondary outline" style="margin-top: 1em;">Clear All Stored Results</button>
</section>
<section id="visualization-section">
<h2>Visualize Stored Data</h2>
<form id="visualization-form">
<label for="viz-result-select">Select Stored Result</label>
<select id="viz-result-select" name="viz-result-select" required>
<option value="" disabled selected>-- Select Result --</option>
</select>
<div id="visualization-options"> <label for="viz-type">Visualization Type</label>
<select id="viz-type" name="viz-type">
<option value="table" selected>Table</option>
<option value="chart-line">Line Chart</option>
<option value="chart-bar">Bar Chart</option>
<option value="chart-pie">Pie Chart</option>
</select>
<div class="grid" id="chart-axis-selectors" style="display: none; margin-top: 1em;"> <label for="viz-xaxis-col">X-Axis Column
<select id="viz-xaxis-col" name="viz-xaxis-col"></select>
</label>
<label for="viz-yaxis-col">Y-Axis Column (Value)
<select id="viz-yaxis-col" name="viz-yaxis-col"></select>
</label>
</div>
<button type="submit" id="generate-viz-btn" style="margin-top: 1em;">Generate Visualization</button>
</div>
</form>
</section>
<section id="results-output-section">
<h2>Visualization Output</h2>
<article id="results-output">
<p>Select a stored result and configure visualization options above.</p>
<div id="table-container" style="overflow-x: auto;"></div>
<div id="chart-container" style="display: none;">
<canvas id="results-chart"></canvas>
</div>
<div id="error-output"></div>
</article>
</section>
</main>
<script>
// --- Constants ---
const CONNECTIONS_STORAGE_KEY = 'appInsightsConnections';
const RESULTS_STORAGE_KEY = 'appInsightsQueryResults';
// --- DOM Elements ---
// Connections
const connectionForm = document.getElementById('connection-form');
const connNameInput = document.getElementById('conn-name');
const connAppIdInput = document.getElementById('conn-appid');
const connApiKeyInput = document.getElementById('conn-apikey');
const connectionsListUl = document.getElementById('connections-ul');
const noConnectionsMsg = document.getElementById('no-connections-msg');
const queryConnectionSelect = document.getElementById('query-connection');
// Querying
const queryForm = document.getElementById('query-form');
const queryKqlTextarea = document.getElementById('query-kql');
const querySaveNameInput = document.getElementById('query-save-name');
const runQueryBtn = document.getElementById('run-query-btn');
const queryStatus = document.getElementById('query-status');
// Stored Results
const storedResultsUl = document.getElementById('results-ul');
const noResultsMsg = document.getElementById('no-results-msg');
const clearResultsBtn = document.getElementById('clear-results-btn');
// Visualization
const visualizationForm = document.getElementById('visualization-form');
const vizResultSelect = document.getElementById('viz-result-select');
const vizOptionsDiv = document.getElementById('visualization-options');
const vizTypeSelect = document.getElementById('viz-type');
const chartAxisSelectors = document.getElementById('chart-axis-selectors');
const vizXAxisSelect = document.getElementById('viz-xaxis-col');
const vizYAxisSelect = document.getElementById('viz-yaxis-col');
const generateVizBtn = document.getElementById('generate-viz-btn');
// Output
const resultsOutput = document.getElementById('results-output');
const tableContainer = document.getElementById('table-container');
const chartContainer = document.getElementById('chart-container');
const chartCanvas = document.getElementById('results-chart');
const errorOutput = document.getElementById('error-output');
let chartInstance = null; // To hold the Chart.js instance
// --- State Management (localStorage) ---
// Connections
function getConnections() {
const connectionsJson = localStorage.getItem(CONNECTIONS_STORAGE_KEY);
return connectionsJson ? JSON.parse(connectionsJson) : [];
}
function saveConnections(connections) {
localStorage.setItem(CONNECTIONS_STORAGE_KEY, JSON.stringify(connections));
renderConnections();
}
function addConnection(name, appId, apiKey) {
const connections = getConnections();
const id = `conn-${Date.now()}`;
if (connections.some(c => c.name === name)) {
alert(`Connection name "${name}" already exists.`); return;
}
connections.push({ id, name, appId, apiKey });
saveConnections(connections);
}
function deleteConnection(id) {
let connections = getConnections();
connections = connections.filter(c => c.id !== id);
saveConnections(connections);
}
// Stored Results
function getStoredResults() {
const resultsJson = localStorage.getItem(RESULTS_STORAGE_KEY);
return resultsJson ? JSON.parse(resultsJson) : [];
}
function saveStoredResults(results) {
try {
localStorage.setItem(RESULTS_STORAGE_KEY, JSON.stringify(results));
} catch (e) {
// Handle potential storage quota exceeded error
console.error("Error saving results to localStorage:", e);
alert("Error saving results: LocalStorage quota might be exceeded. Clear some stored results.");
// Optionally, try to remove the oldest result?
if (e.name === 'QuotaExceededError' && results.length > 0) {
console.warn("Attempting to remove oldest result to free space...");
saveStoredResults(results.slice(1)); // Remove the first (oldest) item
}
}
renderStoredResults();
}
function addStoredResult(result) {
const results = getStoredResults();
// Add metadata if not present
if (!result.id) result.id = `res-${Date.now()}`;
if (!result.timestamp) result.timestamp = new Date().toISOString();
results.push(result);
saveStoredResults(results);
}
function deleteStoredResult(id) {
let results = getStoredResults();
results = results.filter(r => r.id !== id);
saveStoredResults(results);
// If the deleted result was selected, reset visualization form
if (vizResultSelect.value === id) {
vizResultSelect.value = "";
vizOptionsDiv.style.display = 'none';
clearVisualizationOutput();
}
}
function clearAllStoredResults() {
if (confirm("Are you sure you want to delete ALL stored query results? This cannot be undone.")) {
saveStoredResults([]);
vizResultSelect.value = "";
vizOptionsDiv.style.display = 'none';
clearVisualizationOutput();
}
}
// --- Rendering ---
function renderConnections() {
const connections = getConnections();
connectionsListUl.innerHTML = '';
queryConnectionSelect.innerHTML = '<option value="" disabled selected>-- Select --</option>';
noConnectionsMsg.style.display = connections.length === 0 ? 'block' : 'none';
connections.forEach(conn => {
const li = document.createElement('li');
li.innerHTML = `<span>${conn.name} <small>(App ID: ${conn.appId.substring(0, 8)}...)</small></span>`;
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.onclick = () => { if (confirm(`Delete connection "${conn.name}"?`)) deleteConnection(conn.id); };
li.appendChild(deleteBtn);
connectionsListUl.appendChild(li);
const option = document.createElement('option');
option.value = conn.id;
option.textContent = conn.name;
queryConnectionSelect.appendChild(option);
});
}
function renderStoredResults() {
const results = getStoredResults();
storedResultsUl.innerHTML = '';
vizResultSelect.innerHTML = '<option value="" disabled selected>-- Select Result --</option>'; // Reset viz dropdown
noResultsMsg.style.display = results.length === 0 ? 'block' : 'none';
results.forEach(res => {
// List item
const li = document.createElement('li');
const nameText = res.name || `Result from ${new Date(res.timestamp).toLocaleString()}`;
const rowCount = res.data?.tables?.[0]?.rows?.length ?? 0;
li.innerHTML = `<span>${nameText} <small>(${rowCount} rows, Query: ${res.query.substring(0, 30)}...)</small></span>`;
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.onclick = () => deleteStoredResult(res.id);
li.appendChild(deleteBtn);
storedResultsUl.appendChild(li);
// Viz dropdown option
const option = document.createElement('option');
option.value = res.id;
option.textContent = nameText;
vizResultSelect.appendChild(option);
});
}
function displayQueryStatus(message, isError = false) {
queryStatus.textContent = message;
queryStatus.className = isError ? 'error-message' : 'success-message';
// Clear message after a few seconds
setTimeout(() => { queryStatus.textContent = ''; queryStatus.className = ''; }, isError ? 5000 : 3000);
}
function clearVisualizationOutput() {
tableContainer.innerHTML = '';
chartContainer.style.display = 'none';
if (chartInstance) {
chartInstance.destroy();
chartInstance = null;
}
errorOutput.innerHTML = '';
const placeholder = resultsOutput.querySelector('p');
if(!placeholder) { // Add placeholder back if it was removed
const p = document.createElement('p');
p.textContent = 'Select a stored result and configure visualization options above.';
resultsOutput.prepend(p);
}
}
function renderErrorInOutput(message) {
clearVisualizationOutput();
errorOutput.innerHTML = `<div class="error-message">${message}</div>`;
console.error("Visualization Error:", message);
const placeholder = resultsOutput.querySelector('p');
if(placeholder) placeholder.remove();
}
function renderTable(resultData) {
clearVisualizationOutput();
const placeholder = resultsOutput.querySelector('p');
if(placeholder) placeholder.remove();
if (!resultData || !resultData.tables || resultData.tables.length === 0) {
tableContainer.innerHTML = '<p>No data available in the selected result.</p>';
return;
}
const tableData = resultData.tables[0]; // Assume first table
const columns = tableData.columns;
const rows = tableData.rows;
const table = document.createElement('table');
const thead = document.createElement('thead');
const tbody = document.createElement('tbody');
table.className = "striped"; // Pico class
// Header row
const headerRow = document.createElement('tr');
columns.forEach(col => {
const th = document.createElement('th');
th.textContent = `${col.name}`;
th.title = `Type: ${col.type}`
th.scope = 'col';
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
// Data rows
rows.forEach(row => {
const tr = document.createElement('tr');
row.forEach(cell => {
const td = document.createElement('td');
td.textContent = typeof cell === 'object' ? JSON.stringify(cell) : String(cell ?? ''); // Handle null/undefined
tr.appendChild(td);
});
tbody.appendChild(tr);
});
if (rows.length === 0) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = columns.length;
td.textContent = 'Stored result contains no rows.';
td.style.textAlign = 'center';
tr.appendChild(td);
tbody.appendChild(tr);
}
table.appendChild(thead);
table.appendChild(tbody);
tableContainer.appendChild(table);
}
function renderChart(resultData, vizType, xAxisColName, yAxisColName) {
clearVisualizationOutput();
chartContainer.style.display = 'block';
const placeholder = resultsOutput.querySelector('p');
if(placeholder) placeholder.remove();
if (!resultData || !resultData.tables || resultData.tables.length === 0 || resultData.tables[0].rows.length === 0) {
renderErrorInOutput('No data available in the selected result to render chart.');
chartContainer.style.display = 'none';
return;
}
if (!xAxisColName || !yAxisColName) {
renderErrorInOutput('Please select columns for both X and Y axes for the chart.');
chartContainer.style.display = 'none';
return;
}
const tableData = resultData.tables[0];
const columns = tableData.columns;
const rows = tableData.rows;
const xAxisIndex = columns.findIndex(c => c.name === xAxisColName);
const yAxisIndex = columns.findIndex(c => c.name === yAxisColName);
if (xAxisIndex === -1 || yAxisIndex === -1) {
renderErrorInOutput(`Selected column names ('${xAxisColName}', '${yAxisColName}') not found in data.`);
chartContainer.style.display = 'none';
return;
}
const xAxisType = columns[xAxisIndex].type;
const yAxisType = columns[yAxisIndex].type;
// Basic check for Y-axis numeric type
if (!['real', 'long', 'int', 'decimal'].includes(yAxisType.toLowerCase())) {
console.warn(`Y-axis column '${yAxisColName}' has type '${yAxisType}', attempting to parse as number.`);
}
// --- Data Transformation for Chart.js ---
let chartLabels = [];
let chartDataValues = [];
let chartDataType = 'category'; // Default axis type
try {
// Sort data for line charts if X axis is time
if (vizType === 'line' && xAxisType.toLowerCase() === 'datetime') {
rows.sort((a, b) => new Date(a[xAxisIndex]) - new Date(b[xAxisIndex]));
}
rows.forEach(row => {
chartLabels.push(row[xAxisIndex]);
const yVal = parseFloat(row[yAxisIndex]); // Attempt to convert Y to number
chartDataValues.push(isNaN(yVal) ? null : yVal); // Push null if conversion fails
});
// Check if X axis looks like time data
if (xAxisType.toLowerCase() === 'datetime') {
// Ensure labels are actual Date objects or compatible strings for time adapter
chartLabels = chartLabels.map(label => label ? new Date(label) : null); // Convert to Date objects
chartDataType = 'time';
}
} catch (e) {
renderErrorInOutput(`Error processing data for chart: ${e.message}`);
chartContainer.style.display = 'none';
return;
}
// --- End Data Transformation ---
const chartConfig = {
type: vizType, // 'bar', 'line', 'pie'
data: {
labels: chartLabels,
datasets: [{
label: `${yAxisColName} by ${xAxisColName}`,
data: chartDataValues,
borderWidth: vizType === 'line' ? 2 : 1,
fill: vizType === 'line', // Fill area for line charts
// Add dynamic colors later?
backgroundColor: vizType === 'pie' ? generateColors(chartLabels.length) : undefined, // Colors for pie
}]
},
options: {
responsive: true,
maintainAspectRatio: false, // Allow chart to fill container height
scales: (vizType !== 'pie') ? {
x: {
type: chartDataType, // 'category' or 'time'
title: { display: true, text: xAxisColName },
time: (chartDataType === 'time') ? {
// tooltipFormat: 'PPpp', // Customize tooltip format if needed
unit: 'day' // Auto-detect or set based on data range
} : undefined,
},
y: {
beginAtZero: true,
title: { display: true, text: yAxisColName }
}
} : {},
plugins: {
title: {
display: true,
text: `Chart: ${yAxisColName} by ${xAxisColName}`
},
legend: {
display: vizType === 'pie' || false, // Only show legend for Pie for now
position: 'top',
},
tooltip: {
enabled: true,
}
}
}
};
chartInstance = new Chart(chartCanvas, chartConfig);
}
// Helper function to generate distinct colors for Pie charts
function generateColors(count) {
const colors = [];
for (let i = 0; i < count; i++) {
// Simple HSL-based color generation
const hue = (i * (360 / (count * 1.1))) % 360; // Spread hues
colors.push(`hsl(${hue}, 70%, 60%)`);
}
return colors;
}
// --- API Call ---
async function executeAppInsightsQuery(appId, apiKey, kqlQuery) {
const endpoint = `https://api.applicationinsights.io/v1/apps/${appId}/query`;
console.log(`Querying endpoint: ${endpoint}`);
console.warn("Sending API Key directly from browser!"); // Security Reminder
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
body: JSON.stringify({ query: kqlQuery }),
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`API Error (${response.status}): ${errorBody || response.statusText}`);
}
return await response.json(); // Return the parsed data
} catch (error) {
console.error('App Insights API Fetch Error:', error);
throw error;
}
}
// --- Event Listeners ---
connectionForm.addEventListener('submit', (event) => {
event.preventDefault();
const name = connNameInput.value.trim();
const appId = connAppIdInput.value.trim();
const apiKey = connApiKeyInput.value.trim();
if (name && appId && apiKey) {
addConnection(name, appId, apiKey);
connectionForm.reset(); // Clear form
} else { alert('Please fill in all connection fields.'); }
});
queryForm.addEventListener('submit', async (event) => {
event.preventDefault();
const selectedConnectionId = queryConnectionSelect.value;
const kql = queryKqlTextarea.value.trim();
const saveName = querySaveNameInput.value.trim();
if (!selectedConnectionId || !kql) {
displayQueryStatus('Please select a connection and enter a KQL query.', true); return;
}
const connections = getConnections();
const connection = connections.find(c => c.id === selectedConnectionId);
if (!connection) { displayQueryStatus('Selected connection not found.', true); return; }
runQueryBtn.setAttribute('aria-busy', 'true'); runQueryBtn.disabled = true;
queryStatus.textContent = 'Running query...'; queryStatus.className = ''; // Reset status
try {
const resultsData = await executeAppInsightsQuery(connection.appId, connection.apiKey, kql);
displayQueryStatus('Query successful!', false);
// Save result if a name was provided
if (saveName) {
addStoredResult({
name: saveName,
query: kql,
connectionName: connection.name, // Store connection name for context
connectionId: connection.id, // Maybe needed later?
timestamp: new Date().toISOString(),
data: resultsData // Store the actual data payload
});
querySaveNameInput.value = ''; // Clear save name field
} else {
// Optionally display results immediately if not saved?
// renderTable(resultsData); // Or render a temporary view
console.log("Query run but result not saved (no name provided).");
}
} catch (error) {
displayQueryStatus(`Query failed: ${error.message}`, true);
} finally {
runQueryBtn.setAttribute('aria-busy', 'false'); runQueryBtn.disabled = false;
}
});
clearResultsBtn.addEventListener('click', clearAllStoredResults);
// Visualization Form Logic
vizResultSelect.addEventListener('change', (event) => {
const selectedResultId = event.target.value;
clearVisualizationOutput(); // Clear previous output
vizOptionsDiv.style.display = 'none'; // Hide options initially
vizTypeSelect.value = 'table'; // Reset viz type
chartAxisSelectors.style.display = 'none'; // Hide axis selectors
if (selectedResultId) {
const results = getStoredResults();
const selectedResult = results.find(r => r.id === selectedResultId);
if (selectedResult && selectedResult.data && selectedResult.data.tables && selectedResult.data.tables.length > 0) {
const columns = selectedResult.data.tables[0].columns;
populateColumnSelectors(columns);
vizOptionsDiv.style.display = 'block'; // Show options panel
} else {
renderErrorInOutput("Selected result has no data or is invalid.");
}
}
});
vizTypeSelect.addEventListener('change', (event) => {
const type = event.target.value;
if (type === 'table') {
chartAxisSelectors.style.display = 'none';
} else {
// Repopulate just in case columns weren't ready before
const selectedResultId = vizResultSelect.value;
if(selectedResultId) {
const results = getStoredResults();
const selectedResult = results.find(r => r.id === selectedResultId);
if (selectedResult?.data?.tables?.[0]?.columns) {
populateColumnSelectors(selectedResult.data.tables[0].columns); // Ensure selectors have columns
}
}
chartAxisSelectors.style.display = 'grid'; // Show axis selectors for charts
}
clearVisualizationOutput(); // Clear output when type changes
});
visualizationForm.addEventListener('submit', (event) => {
event.preventDefault();
const selectedResultId = vizResultSelect.value;
const vizType = vizTypeSelect.value; // 'table' or 'chart-...'
const xAxis = vizXAxisSelect.value;
const yAxis = vizYAxisSelect.value;
if (!selectedResultId) {
renderErrorInOutput("Please select a stored result first."); return;
}
const results = getStoredResults();
const selectedResult = results.find(r => r.id === selectedResultId);
if (!selectedResult || !selectedResult.data) {
renderErrorInOutput("Could not find data for the selected result."); return;
}
if (vizType === 'table') {
renderTable(selectedResult.data);
} else if (vizType.startsWith('chart-')) {
const chartType = vizType.split('-')[1]; // bar, line, pie
if (!xAxis || !yAxis) {
renderErrorInOutput("Please select columns for both X and Y axes for charts."); return;
}
renderChart(selectedResult.data, chartType, xAxis, yAxis);
} else {
renderErrorInOutput("Invalid visualization type selected.");
}
});
function populateColumnSelectors(columns) {
vizXAxisSelect.innerHTML = '<option value="" disabled selected>-- Select --</option>';
vizYAxisSelect.innerHTML = '<option value="" disabled selected>-- Select --</option>';
columns.forEach(col => {
const optionX = document.createElement('option');
optionX.value = col.name;
optionX.textContent = `${col.name} (${col.type})`;
vizXAxisSelect.appendChild(optionX);
// Only add numeric types to Y-axis selector? (Optional strictness)
// if (['real', 'long', 'int', 'decimal'].includes(col.type.toLowerCase())) {
const optionY = document.createElement('option');
optionY.value = col.name;
optionY.textContent = `${col.name} (${col.type})`;
vizYAxisSelect.appendChild(optionY);
// }
});
}
// --- Initialization ---
document.addEventListener('DOMContentLoaded', () => {
renderConnections();
renderStoredResults();
});
</script>
</body>
</html>
@kchaitanya863
Copy link
Author

Standalone Azure Application Insights Query & Visualization Tool

This is a single HTML file application that allows you to connect to Azure Application Insights, execute KQL queries, store the results in your browser's localStorage, and then create basic tables and charts from that stored data. It requires no backend or build process – just save and open in a browser.

Technology:

Core Functionality:

  • Connection Management: Add and save App Insights connection details (Name, App ID, API Key) to localStorage.
  • KQL Query Execution: Run KQL queries against a selected connection via the App Insights REST API.
  • Result Storage: Optionally name and save successful query results (including the data payload) to localStorage.
  • Result Management: View a list of stored results and delete individual results or clear all stored data.
  • Data Visualization:
    • Select a previously stored query result.
    • Choose to display it as a Table.
    • Choose to display it as a Chart (Line, Bar, Pie).
    • Select appropriate columns from the stored data for chart axes (X and Y).

How to Use:

  1. Copy the code from the Gist file.
  2. Save it locally with an .html extension (e.g., appinsights_tool.html).
  3. Open the HTML file in your preferred web browser.
  4. Add a connection using your App Insights App ID and an API Key (generate one in the Azure portal with query permissions).
  5. Run queries, save results, and visualize them.

🚨 CRITICAL SECURITY WARNING 🚨
This tool operates entirely client-side. Your App Insights API Key is entered into the browser, stored in localStorage, and sent directly in API requests visible in browser developer tools. This is fundamentally insecure and exposes your API key.

  • DO NOT use this tool in production environments.
  • DO NOT use it on shared or public computers.
  • DO NOT use it for highly sensitive Application Insights data.
  • This is only suitable for isolated, personal testing by users who understand and explicitly accept the associated security risks.
  • For secure applications, always handle API keys on a trusted server-side backend/proxy.

Limitations:

  • Security: API Key exposure is a major risk.
  • Persistence: localStorage is browser-specific and can be cleared or exceed size limits (typically 5-10MB).
  • UI: Basic UI, no advanced features like draggable dashboards.
  • Scalability: Not suitable for very large result sets or complex workflows.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment