Introduzione
Grafana è una delle piattaforme open source di osservabilità più popolari, con oltre 70.000 stelle su GitHub e migliaia di collaboratori che la migliorano ogni giorno. Con più di 3.000 issue aperte e centinaia di pull request costantemente in corso, mantenere il codice pulito e coerente rappresenta una vera sfida.
Analizzando il codice sorgente, possiamo individuare alcune delle regole non scritte e delle best practice che aiutano il team a mantenere un elevato livello di qualità pur procedendo rapidamente. Molte di queste regole si concentrano su sicurezza, manutenibilità e affidabilità. Alcune riguardano problemi che gli strumenti tradizionali di analisi statica (SAST) non riescono a individuare, come un uso improprio dell’asincronia, perdite di risorse o modelli incoerenti nel codice. Si tratta del tipo di problemi che i revisori umani o gli strumenti basati sull’intelligenza artificiale possono individuare durante una revisione del codice.
Le sfide
Progetti di grandi dimensioni come questo devono affrontare diverse sfide: un volume enorme di codice, numerosi moduli (API, interfaccia utente, plugin) e innumerevoli integrazioni esterne (Prometheus, Loki, ecc.). Centinaia di collaboratori possono seguire stili di programmazione o presupposti diversi. Le nuove funzionalità e le correzioni rapide possono introdurre bug nascosti, falle di sicurezza o percorsi di codice confusi. I revisori volontari potrebbero non conoscere ogni parte del codice, con il rischio di trascurare alcuni modelli di progettazione o le migliori pratiche. In breve, la portata e la diversità dei contributi rendono difficile garantire coerenza e affidabilità.
Perché queste regole sono importanti
Una serie chiara di regole di revisione va a diretto vantaggio dello stato di salute di Grafana. Innanzitutto, migliora la manutenibilità: modelli coerenti (struttura delle cartelle, denominazione, gestione degli errori) rendono il codice più facile da leggere, testare ed estendere. I revisori impiegano meno tempo a indovinare le intenzioni quando tutti seguono convenzioni comuni. In secondo luogo, la sicurezza risulta rafforzata: regole come “convalidare sempre gli input degli utenti” o “evitare i reindirizzamenti aperti” prevengono le vulnerabilità (CVE-2025-6023/4123, ecc.) che sono state individuate in Grafana. Infine, l’inserimento dei nuovi collaboratori è più rapido: quando gli esempi e le revisioni utilizzano in modo coerente le stesse pratiche, i nuovi arrivati imparano il “modo Grafana” rapidamente e con sicurezza.
Collegare il contesto a queste regole
Queste regole derivano da problemi reali riscontrati nel codice e nella comunità di Grafana. Le segnalazioni di sicurezza e i rapporti sui bug hanno messo in luce alcuni schemi ricorrenti (ad esempio, il path traversal che porta a un attacco XSS) che abbiamo trasformato in regole preventive. Ciascuna delle regole riportate di seguito evidenzia una trappola concreta, spiega perché è importante (prestazioni, chiarezza, sicurezza, ecc.) e mostra un chiaro confronto tra uno snippet ❌ non conforme e uno ✅ conforme nei linguaggi utilizzati da Grafana (Go o TypeScript/JS).
Ora vediamo le 10 regole che contribuiscono a mantenere il codice di Grafana solido, sicuro e comprensibile.
10 regole pratiche per la qualità del codice ispirate a Grafana
1. Utilizzare le variabili d'ambiente per la configurazione (evitare valori fissi).
Evita diinserire direttamente nel codice porte, credenziali, URL o altri valori specifici dell'ambiente. Leggili dalle variabili d'ambiente o dai file di configurazione per mantenere il codice flessibile e tenere le informazioni riservate fuori dal codice sorgente.
❌ Non conforme:
// server.js
const appPort = 3000;
app.listen(appPort, () => console.log("In ascolto sulla porta " + appPort));✅ Conforme:
// server.ts
const PORT = Number(process.env.PORT) || 3000;
app.listen(PORT, () => console.log(`Listening on port ${PORT}`));Perché è importante: L'uso delle variabili d'ambiente mantiene i dati sensibili fuori dal codice sorgente, rende le distribuzioni flessibili in diversi ambienti ed evita la divulgazione accidentale di informazioni riservate. Inoltre, garantisce che le modifiche alla configurazione non richiedano modifiche al codice, migliorando la manutenibilità e riducendo gli errori.
2. Esegui la sanificazione dei dati inseriti dall'utente prima di utilizzarli.
Tutti i dati provenienti dagli utenti o da fonti esterne devono essere convalidati o sottoposti a sanificazione prima dell'utilizzo, al fine di prevenire attacchi di tipo "injection" e comportamenti imprevisti.
❌ Non conforme:
// frontend/src/components/UserForm.tsx
const handleSubmit = (username: string) => {
setUsers([...users, { name: username }]);
};✅ Conforme:
// frontend/src/utils/sanitize.ts
export function sanitizeInput(input: string): string {
return input.replace(/<[^>]*>/g, ''); // removes HTML tags
}
// frontend/src/components/UserForm.tsx
import { sanitizeInput } from '../utils/sanitize';
const handleSubmit = (username: string) => {
const cleanName = sanitizeInput(username);
setUsers([...users, { name: cleanName }]);
};Perché è importante: Una corretta sanificazione degli input previene attacchi XSS, attacchi di tipo injection e comportamenti imprevisti causati da input non validi. Protegge sia gli utenti che il sistema e garantisce che i processi a valle, la registrazione e l'archiviazione gestiscano i dati in modo sicuro.
3. Prevenire i reindirizzamenti aperti e il path traversal.
Assicurati che tutti gli URL o i percorsi dei file utilizzati nel tuo codice siano correttamente convalidati e sanificati. Non consentire che gli input dell'utente determinino direttamente i reindirizzamenti o i percorsi del filesystem.
❌ Non conforme:
// Express route in Grafana plugin
app.get("/goto", (req, res) => {
const dest = req.query.next; // attacker can supply any URL
res.redirect(dest);
});✅ Conforme:
// Express route with safe redirect
app.get("/goto", (req, res) => {
const dest = req.query.next;
// Only allow relative paths starting with '/'
if (dest && dest.startsWith("/")) {
res.redirect(dest);
} else {
res.status(400).send("Invalid redirect URL");
}
});Perché è importante: Prevenire i reindirizzamenti aperti e il path traversal protegge gli utenti dal phishing, dalle fughe di dati e dall'accesso non autorizzato ai file. Riduce la superficie di attacco, rafforza i confini di sicurezza ed evita l'esposizione accidentale di risorse sensibili del server.
4. Attivare una politica di sicurezza dei contenuti (CSP) rigorosa.
Applicare una politica di sicurezza dei contenuti (Content Security Policy) nelle intestazioni dell'applicazione che consenta l'uso solo di script, fogli di stile, immagini e altre risorse provenienti da fonti attendibili. Impedire l'uso di "unsafe-inline", "eval" e fonti con caratteri jolly.
❌ Non conforme: (mancanza di CSP o criteri troppo permissivi)
# grafana.ini (non conforme)
content_security_policy = false✅ Conforme: (CSP rigoroso nella configurazione)
# grafana.ini
content_security_policy = true
content_security_policy_template = """
script-src 'self' 'unsafe-eval' 'unsafe-inline' 'strict-dynamic' $NONCE;
object-src 'none';
font-src 'self';
style-src 'self' 'unsafe-inline' blob:;
img-src * data:;
base-uri 'self';
connect-src 'self' grafana.com ws://$ROOT_PATH wss://$ROOT_PATH;
manifest-src 'self';
media-src 'none';
form-action 'self';
"""Perché è importante: Un CSP rigoroso blocca molte categorie di attacchi lato client, tra cui l'XSS. Impone un comportamento prevedibile alle risorse, riduce la possibilità di esecuzione di codice dannoso e fornisce un chiaro confine di sicurezza nel contesto del browser.
5. Gestire gli errori e i controlli per i valori nil (evitare i panici).
Verificare sempre la presenza di errori e valori nulli nelle chiamate alle funzioni, nelle risposte delle API e nelle strutture dati. Sostituire i panici con un'adeguata gestione degli errori e restituire messaggi o codici di errore significativi.
❌ Non conforme:
rows, _ := db.Query("SELECT * FROM users WHERE id=?", id) // ignored error
user := &User{}
rows.Next()
rows.Scan(&user.Name) // rows might be empty => user is nil => panic✅ Conforme:
rows, err := db.Query("SELECT * FROM users WHERE id=?", id)
if err != nil {
return nil, err
}
defer rows.Close()
if !rows.Next() {
return nil, errors.New("user not found")
}
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
user := &User{Name: name}Perché è importante: Una corretta gestione degli errori previene i crash e garantisce l'affidabilità del sistema anche in presenza di input o condizioni impreviste. Migliora la manutenibilità, riduce i tempi di inattività e semplifica il debug fornendo informazioni significative sugli errori.
6. Rinviare la pulizia delle risorse (per evitare perdite).
Assicurarsi che tutte le risorse aperte, quali file, connessioni di rete o handle di database, vengano correttamente chiuse utilizzando `defer` immediatamente dopo l'allocazione. Non fare affidamento su operazioni di pulizia manuali in punti successivi del codice.
❌ Non conforme:
resp, err := http.Get(url)
// ... usa resp.Body ...
// dimenticato: resp.Body.Close()✅ Conforme:
resp, err := http.Get(url)
if err != nil {
// handle error
}
defer resp.Body.Close()
// ... use resp.Body ...Perché è importante: Una corretta pulizia previene le perdite di memoria, l'esaurimento dei descrittori di file e la saturazione del pool di connessioni. Ciò garantisce la stabilità del sistema, evita il degrado delle prestazioni nel tempo e riduce i problemi operativi in produzione.
7. Utilizzare query parametrizzate (per evitare attacchi di tipo SQL injection).
Quando si interagisce con il database, utilizzare sempre query parametrizzate o istruzioni preparate, anziché la concatenazione di stringhe per i comandi SQL.
❌ Non conforme:
// Pericolo: userID potrebbe contenere un apice SQL o un codice di iniezione
query := "DELETE FROM sessions WHERE user_id = '" + userID + "';"
db.Exec(query)✅ Conforme:
// Sicuro: userID viene passato come parametro
db.Exec("DELETE FROM sessions WHERE user_id = ?", userID)Perché è importante: Le query parametrizzate prevengono gli attacchi di tipo SQL injection, una delle vulnerabilità di sicurezza più comuni. Proteggono i dati sensibili, riducono il rischio di danneggiamento del database e rendono le query più gestibili e più facili da verificare. Ciò garantisce sia la sicurezza che l'affidabilità della vostra applicazione.
8. Utilizzare correttamente async/await in TypeScript (gestire le promesse).
Attendere semprele promesse e gestire gli errori utilizzando try/catch, invece di ignorare i rifiuti o combinare la gestione in stile callback.
❌ Non conforme:
async function fetchData() {
// Missing await: fetch returns a Promise, not the actual data
const res = fetch('/api/values');
console.log(res.data); // undefined
}✅ Conforme:
async function fetchData() {
try {
const res = await fetch('/api/values');
const data = await res.json();
console.log(data);
} catch (err) {
console.error("Fetch failed:", err);
}
}Perché è importante: Una corretta gestione asincrona garantisce che gli errori nel codice asincrono non passino inosservati, impedisce il rifiuto di promesse non gestite e mantiene un flusso di programma prevedibile. Rende il codice più leggibile, più facile da debuggare e previene bug sottili che possono portare alla corruzione dei dati, a stati incoerenti o a arresti imprevisti durante l'esecuzione.
9. Prediligere i tipi rigorosi in TypeScript (evitare quelli generici).
Utilizza tipi TypeScript precisi anziché "any" per definire variabili, parametri di funzione e tipi di ritorno.
❌ Non conforme:
// No types specified
function updateUser(data) {
// ...
}
let config: any = loadConfig();✅ Conforme:
interface User { id: number; name: string; }
function updateUser(data: User): Promise<User> {
// ...
}
interface AppConfig { endpoint: string; timeoutMs: number; }
const config: AppConfig = loadConfig();Perché è importante: La tipizzazione rigorosa rileva gli errori relativi ai tipi in fase di compilazione, riducendo gli errori di esecuzione e migliorando l’affidabilità del codice. Rende il codice autodocumentante, più facile da rifattorizzare e garantisce che tutte le parti del sistema interagiscano in modo prevedibile e sicuro dal punto di vista dei tipi, il che è fondamentale in codebase grandi e complesse come quella di Grafana.
10. Utilizzare uno stile di codifica e una convenzione di denominazione coerenti.
Garantire l'uniformità della formattazione, delle convenzioni di denominazione e delle strutture dei file in tutto il codice.
❌ Non conforme: (stili misti)
const ApiData = await getdata(); // PascalCase for variable? function name not camelCase.
function Fetch_User() { ... } // Unusual naming.✅ Conforme:
const apiData = await fetchData();
function fetchUser() { ... }Perché è importante: Uno stile e una nomenclatura coerenti migliorano la leggibilità e facilitano la comprensione e la manutenzione del codice da parte di più collaboratori. Riducono lo sforzo cognitivo necessario per orientarsi nel progetto, prevengono bug impercettibili causati da fraintendimenti e garantiscono che gli strumenti automatizzati (linter, formattatori, revisori di codice) possano applicare in modo affidabile gli standard di qualità in un ambiente con un team numeroso.
Conclusione
Ciascuna delle regole sopra riportate affronta una sfida ricorrente nel codice di Grafana. Applicarle in modo coerente durante le revisioni del codice aiuta il team a mantenere un codice pulito e prevedibile, a migliorare la sicurezza prevenendo le vulnerabilità più comuni e a rendere più agevole l’inserimento dei nuovi collaboratori, fornendo loro modelli chiari a cui fare riferimento. Man mano che il progetto cresce, queste pratiche garantiscono che il codice rimanga affidabile, gestibile e più facile da consultare per tutte le persone coinvolte. Seguire queste regole può aiutare qualsiasi team di ingegneri a sviluppare e mantenere software di alta qualità su larga scala.

