Introduzione
Grafana è una delle piattaforme di osservabilità open source più popolari, con oltre 70.000 stelle su GitHub e migliaia di contributori che la migliorano ogni giorno. Con più di 3.000 problemi aperti e centinaia di pull request costantemente in movimento, mantenere la codebase pulita e coerente è una vera sfida.
Studiando la sua codebase, possiamo scoprire alcune delle regole non scritte e delle best practice che aiutano il team a mantenere alta la qualità procedendo rapidamente. Molte di queste regole si concentrano su sicurezza, manutenibilità e affidabilità. Alcune affrontano problemi che i tradizionali strumenti di analisi statica (SAST) non riescono a rilevare, come l'uso improprio di funzioni asincrone, perdite di risorse o pattern incoerenti nel codice. Questi sono i tipi di problemi che i revisori umani o gli strumenti basati sull'IA possono individuare durante una code review.
Le sfide
Grandi progetti come questo affrontano diverse sfide: un volume di codice massiccio, numerosi moduli (API, UI, plugin) e innumerevoli integrazioni esterne (Prometheus, Loki, ecc.). Centinaia di contributori possono seguire stili di codifica o presupposti differenti. Nuove funzionalità e correzioni rapide possono introdurre bug nascosti, problemi di sicurezza o percorsi di codice confusi. I revisori volontari potrebbero non conoscere ogni parte della codebase, portando a pattern di progettazione o best practice trascurati. In breve, la scala e la diversità dei contributi rendono difficile imporre coerenza e affidabilità.
Perché queste regole sono importanti
Un insieme chiaro di regole di revisione avvantaggia direttamente la salute di Grafana. Innanzitutto, migliora la mantenibilità: pattern consistenti (struttura delle cartelle, denominazione, gestione degli errori) rendono il codice più facile da leggere, testare ed estendere. I revisori perdono meno tempo a indovinare le intenzioni quando tutti seguono convenzioni comuni. In secondo luogo, viene rafforzata la sicurezza: regole come "valida sempre l'input dell'utente" o "evita i reindirizzamenti aperti" prevengono vulnerabilità (CVE-2025-6023/4123, ecc.) riscontrate in Grafana. Infine, l'onboarding dei nuovi contributori è più rapido: quando gli esempi e le revisioni utilizzano costantemente le stesse pratiche, i nuovi arrivati imparano lo "stile Grafana" rapidamente e con sicurezza.
Contestualizzazione di queste regole
Queste regole derivano da problemi reali nel codice e nella community di Grafana. Avvisi di sicurezza e bug report hanno portato alla luce pattern (ad esempio, il path traversal che porta a XSS) che trasformiamo in regole preventive. Ciascuna regola di seguito evidenzia una criticità concreta, spiega perché è importante (prestazioni, chiarezza, sicurezza, ecc.) e mostra un chiaro frammento ❌ non conforme rispetto a ✅ conforme nei linguaggi di Grafana (Go o TypeScript/JS).
Ora esploriamo le 10 regole che aiutano a mantenere la codebase di Grafana robusta, sicura e comprensibile.
10 regole pratiche di qualità del codice ispirate a Grafana
1. Utilizzare le variabili d'ambiente per la configurazione (evitare valori hard-coded).
Evita di inserire direttamente nel codice (hard-code) porte, credenziali, URL o altri valori specifici dell'ambiente. Leggili da variabili d'ambiente o file di configurazione per mantenere il codice flessibile ed evitare di lasciare credenziali nel codice sorgente.
❌ Non conforme:
// server.js
const appPort = 3000;
app.listen(appPort, () => console.log("Listening on port " + 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 i rilasci flessibili tra diversi ambienti ed evita la fuga accidentale di segreti. Garantisce inoltre che le modifiche alla configurazione non richiedano modifiche al codice, migliorando la manutenibilità e riducendo gli errori.
2. Sanitizzare l'input dell'utente prima di utilizzarlo.
Tutti gli input provenienti da utenti o fonti esterne devono essere convalidati o sanitizzati prima dell'uso per prevenire attacchi di 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 sanitizzazione dell'input previene XSS, attacchi di injection e comportamenti anomali causati da input non validi. Protegge sia gli utenti che il sistema e garantisce che i processi a valle, la registrazione dei log e l'archiviazione gestiscano i dati in modo sicuro.
3. Prevenire reindirizzamenti aperti e path traversal.
Assicurati che qualsiasi URL o percorso di file utilizzato nel codice sia adeguatamente convalidato e sanitizzato. Non consentire all'input dell'utente di determinare direttamente reindirizzamenti o percorsi del file system.
❌ 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: La prevenzione di reindirizzamenti aperti e path traversal protegge gli utenti da phishing, fughe di dati e accessi non autorizzati ai file. Riduce la superficie d'attacco, impone i confini di sicurezza ed evita l'esposizione accidentale di risorse di server sensibili.
4. Abilitare una Content Security Policy (CSP) rigorosa.
Implementa una Content Security Policy negli header dell'applicazione che consenta script, stili, immagini e altre risorse solo da fonti attendibili. Disabilita unsafe-inline, eval e origini con carattere jolly (*).
❌ Non conforme: (Nessuna CSP o configurazione troppo permissiva)
# grafana.ini (non conforme)
content_security_policy = false✅ Conforme: (CSP forte 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: Una CSP rigorosa blocca molte classi di attacchi lato client, inclusa la XSS. Garantisce un comportamento prevedibile per le risorse, riduce la probabilità di esecuzione di codice malevolo e fornisce un chiaro confine di sicurezza nel contesto del browser.
5. Gestire gli errori e i controlli sui valori nulli (evitare i panic).
Verifica sempre la presenza di errori e valori nil nelle chiamate di funzione, nelle risposte delle API e nelle strutture dati. Sostituisci i panic con una gestione corretta degli errori e restituisci 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 che il sistema rimanga affidabile anche in presenza di input o condizioni inattese. Migliora la manutenibilità, riduce i tempi di inattività e semplifica il debug fornendo informazioni chiare sugli errori.
6. Rimanda la pulizia delle risorse (prevenzione dei leak).
Assicurati che tutte le risorse aperte, come file, connessioni di rete o handle di database, vengano chiuse correttamente usando defer subito dopo l'allocazione. Non affidarti a operazioni di pulizia manuali eseguite in seguito nel 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 memory leak, esaurimento dei descrittori di file e saturazione del pool di connessioni. Questo mantiene la stabilità del sistema, evita il degrado delle performance nel tempo e riduce i problemi operativi in produzione.
7. Usa query parametrizzate (evita l'SQL injection).
Usa sempre query parametrizzate o prepared statement quando interagisci con il database, anziché la concatenazione di stringhe per i comandi SQL.
❌ Non conforme:
// Pericoloso: userID potrebbe contenere un apice SQL o un'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 SQL injection, una delle vulnerabilità di sicurezza più comuni. Proteggono i dati sensibili, riducono il rischio di corruzione del database e rendono le query più manutenibili e facili da controllare. Questo garantisce sia la sicurezza che l'affidabilità della tua applicazione.
8. Usa async/await correttamente in TypeScript (gestisci le promise).
Usa sempre await con le promise e gestisci gli errori tramite try/catch anziché ignorare i rifiuti (rejections) o mischiare 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 assicura che gli errori nel codice asincrono non passino inosservati, previene promise rejection non gestite e mantiene un flusso del programma prevedibile. Rende il codice più leggibile, semplice da debuggare e previene bug subdoli che possono portare a corruzione dei dati, stati incoerenti o crash imprevisti a runtime.
9. Favorisci i tipi rigidi in TypeScript (evita any).
Usa 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 rigida rileva gli errori legati ai tipi in fase di compilazione, riducendo i problemi a runtime e migliorando l'affidabilità del codice. Rende il codice auto-esplicativo, più facile da rifattorizzare e assicura che tutte le parti del sistema interagiscano in modo prevedibile e type-safe, aspetto cruciale in codebase grandi e complesse come quella di Grafana.
10. Applica uno stile di codice e una convenzione diNaming coerenti.
Imponi una formattazione uniforme, convenzioni di denominazione e strutture di file coerenti in tutta la codebase.
❌ 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 denominazione coerenti migliorano la leggibilità e facilitano la comprensione e la manutenzione del codice da parte di più contributori. Riducono il carico cognitivo durante la navigazione nel progetto, prevengono bug subdoli causati da incomprensioni e assicurano che gli strumenti automatizzati (linter, formattatore, code reviewer) possano applicare in modo affidabile gli standard di qualità in un team numeroso.
Conclusione
Ciascuna delle regole precedenti affronta una sfida ricorrente nella codebase di Grafana. Applicarle in modo coerente durante le code review aiuta il team a mantenere un codice pulito e prevedibile, a migliorare la sicurezza prevenendo vulnerabilità comuni e a semplificare l'onboarding offrendo pattern chiari per i nuovi contributori. Con la crescita del progetto, queste pratiche mantengono la codebase affidabile, manutenibile e semplice da consultare per tutti i soggetti coinvolti. Seguire queste regole può aiutare qualsiasi team di ingegneria a sviluppare e mantenere software di alta qualità su scala.

