Aikido

10 règles de qualité du code apprises auprès de l'équipe d'ingénieurs de Grafana

Introduction

Grafana est l'une des plateformes d'observabilité open-source les plus populaires, avec plus de 70 000 étoiles sur GitHub et des milliers de contributeurs qui l'améliorent chaque jour. Avec plus de 3 000 problèmes ouverts et des centaines de pull requests constamment en mouvement, maintenir la base de code propre et cohérente est un véritable défi.

En étudiant son code source, nous pouvons découvrir certaines des règles tacites et des meilleures pratiques qui aident l'équipe à maintenir un niveau de qualité élevé tout en avançant rapidement. Bon nombre de ces règles sont axées sur la sécurité, la maintenabilité et la fiabilité. Certaines d'entre elles traitent de problèmes que les outils d'analyse statique traditionnels (SAST) ne peuvent pas détecter, comme une utilisation incorrecte de l'asynchronisme, des fuites de ressources ou des modèles incohérents dans le code. Ce sont là des types de problèmes que les réviseurs humains ou les outils basés sur l'IA peuvent repérer lors d'une révision du code.

Les défis

Les projets de cette envergure sont confrontés à plusieurs défis : un volume de code massif, de nombreux modules (API, UI, plugins) et d'innombrables intégrations externes (Prometheus, Loki, etc.). Des centaines de contributeurs peuvent suivre des styles de codage ou des hypothèses différents. Les nouvelles fonctionnalités et les correctifs rapides peuvent introduire des bugs cachés, des failles de sécurité ou des chemins de code complexes. Les relecteurs bénévoles peuvent ne pas connaître chaque partie de la base de code, ce qui entraîne des omissions de modèles de conception ou de bonnes pratiques. En bref, l'ampleur et la diversité des contributions rendent la cohérence et la fiabilité difficiles à garantir.

Pourquoi ces règles sont importantes

Un ensemble clair de règles de révision bénéficie directement à la santé de Grafana. Premièrement, la maintenabilité s'améliore : des modèles cohérents (structure des dossiers, nommage, gestion des erreurs) facilitent la lecture, le test et l'extension du code. Les relecteurs passent moins de temps à deviner les intentions lorsque tout le monde suit les conventions communes. Deuxièmement, la sécurité est renforcée : des règles comme « toujours valider les entrées utilisateur » ou « éviter les redirections ouvertes » préviennent les vulnérabilités (CVE-2025-6023/4123, etc.) qui ont été découvertes dans Grafana. Enfin, l'intégration de nouveaux contributeurs est plus rapide : lorsque les exemples et les révisions utilisent systématiquement les mêmes pratiques, les nouveaux venus apprennent rapidement et en toute confiance la « manière Grafana ».

Relier le contexte à ces règles

Ces règles proviennent de problèmes réels rencontrés dans le code et la communauté de Grafana. Les avis de sécurité et les rapports de bugs ont révélé des schémas (par exemple, la traversée de chemin menant à du XSS) que nous transformons en règles préventives. Chaque règle ci-dessous met en évidence un piège concret, explique pourquoi c'est important (performance, clarté, sécurité, etc.) et montre un extrait clair ❌ non-conforme vs ✅ conforme dans les langages de Grafana (Go ou TypeScript/JS).

Explorons maintenant les 10 règles qui contribuent à maintenir la base de code de Grafana robuste, sécurisée et compréhensible.

10 règles pratiques pour la qualité du code inspirées par Grafana

1. Utiliser des variables d'environnement pour la configuration (éviter les valeurs codées en dur).

Évitez de coder en dur les ports, les identifiants, les URL ou d'autres valeurs spécifiques à l'environnement. Lisez-les à partir de variables d'environnement ou de fichiers de configuration pour maintenir la flexibilité du code et garder les secrets hors du code source.

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}`));

Pourquoi c'est important : L'utilisation de variables d'environnement permet de maintenir les données sensibles hors du code source, rend les déploiements flexibles entre les différents environnements et évite les fuites accidentelles de secrets. Cela garantit également que les modifications de configuration ne nécessitent pas de modifications de code, améliorant ainsi la maintenabilité et réduisant les erreurs.

2. Assainissez les entrées utilisateur avant de les utiliser.

Toutes les entrées provenant d'utilisateurs ou de sources externes doivent être validées ou assainies avant utilisation afin de prévenir les attaques par injection et les comportements inattendus.

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 }]);
};

Pourquoi c'est important : Un assainissement approprié des entrées prévient les attaques XSS, les injections et les comportements inattendus causés par des entrées malformées. Cela protège à la fois les utilisateurs et le système, et garantit que les processus en aval, la journalisation et le stockage traitent les données en toute sécurité.

3. Prévenir les redirections ouvertes et le path traversal.

Assurez-vous que toutes les URL ou chemins de fichiers utilisés dans votre code sont correctement validés et nettoyés. Ne permettez pas aux entrées utilisateur de déterminer directement les redirections ou les chemins du système de fichiers.

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");
  }
});

Pourquoi c'est important : Prévenir les redirections ouvertes et les attaques par traversée de répertoire (path traversal) protège les utilisateurs du phishing, des fuites de données et de l'accès non autorisé aux fichiers. Cela réduit la surface d'attaque, renforce les limites de sécurité et évite l'exposition accidentelle de ressources serveur sensibles.

4. Activer une politique de sécurité de contenu (CSP) stricte.

Appliquez une politique de sécurité du contenu (CSP) dans les en-têtes de l'application qui n'autorise que les scripts, styles, images et autres ressources provenant de sources fiables. Interdisez les sources unsafe-inline, eval et wildcard.

Non conforme : (Pas de CSP ou trop permissif)

# grafana.ini (non-conforme)
content_security_policy = false

Conforme : (CSP robuste dans la configuration)

# 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';
"""

Pourquoi c'est important : Une CSP stricte bloque de nombreuses classes d'attaques côté client, y compris les XSS. Elle impose un comportement prévisible pour les ressources, réduit les risques d'exécution de code malveillant et fournit une limite de sécurité claire dans le contexte du navigateur.

5. Gérer les erreurs et les vérifications de nil (éviter les paniques).

Vérifiez toujours les erreurs et les valeurs nulles dans les appels de fonction, les réponses d'API et les structures de données. Remplacez les paniques par une gestion des erreurs appropriée et retournez des messages ou codes d'erreur significatifs.

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}

Pourquoi c'est important : Une gestion appropriée des erreurs prévient les plantages et garantit que le système reste fiable même lorsque des entrées ou des conditions inattendues se produisent. Elle améliore la maintenabilité, réduit les temps d'arrêt et facilite le débogage en fournissant des informations d'erreur significatives.

6. Différer le nettoyage des ressources (prévenir les fuites).

Assurez-vous que toutes les ressources ouvertes, telles que les fichiers, les connexions réseau ou les handles de base de données, sont correctement fermées en utilisant defer immédiatement après leur allocation. Ne vous fiez pas à un nettoyage manuel ultérieur dans le code.

Non conforme :

resp, err := http.Get(url)
// ... utiliser resp.Body ...
// oublié : resp.Body.Close()

Conforme :

resp, err := http.Get(url)
if err != nil {
    // handle error
}
defer resp.Body.Close()
// ... use resp.Body ...

Pourquoi c'est important : Un nettoyage approprié prévient les fuites de mémoire, l'épuisement des descripteurs de fichiers et la saturation des pools de connexion. Cela maintient la stabilité du système, évite la dégradation des performances au fil du temps et réduit les problèmes opérationnels en production.

7. Utiliser des requêtes paramétrées (éviter l'injection SQL).

Utilisez toujours des requêtes paramétrées ou des requêtes préparées lors de l'interaction avec la base de données, au lieu de la concaténation de chaînes pour les commandes SQL.

Non conforme :

// Dangereux : userID peut contenir une citation SQL ou une injection
query := "DELETE FROM sessions WHERE user_id = '" + userID +"';" db.Exec(query ) + userID + "' ;"
db.Exec(query)

Conforme :

// Sûr : userID est passé en tant que paramètre
db.Exec("DELETE FROM sessions WHERE user_id = ?", userID)

Pourquoi c'est important : Les requêtes paramétrées préviennent les attaques par injection SQL, l'une des vulnérabilités de sécurité les plus courantes. Elles protègent les données sensibles, réduisent le risque de corruption de la base de données et rendent les requêtes plus maintenables et plus faciles à auditer. Cela garantit à la fois la sécurité et la fiabilité de votre application.

8. Utiliser correctement async/await en TypeScript (gérer les promesses).

Toujours attendre les promesses et gérer les erreurs avec try/catch, au lieu d'ignorer les rejets ou de mélanger la gestion de style 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);
  }
}

Pourquoi c'est important : Une gestion asynchrone appropriée garantit que les erreurs dans le code asynchrone ne passent pas inaperçues, prévient les rejets de promesses non gérés et maintient un flux de programme prévisible. Cela rend le code plus lisible, plus facile à déboguer et prévient les bugs subtils qui peuvent entraîner une corruption des données, un état incohérent ou des plantages inattendus à l'exécution (runtime).

9. Privilégier les types stricts en TypeScript (éviter 'any').

Utilisez des types TypeScript précis au lieu de `any` pour définir les variables, les paramètres de fonction et les types de retour.

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();

Pourquoi c'est important : Le typage strict détecte les erreurs liées aux types au moment de la compilation, réduisant ainsi les erreurs d'exécution (runtime) et améliorant la fiabilité du code. Il rend le code auto-documenté, plus facile à refactoriser et garantit que toutes les parties du système interagissent de manière prévisible et sûre en termes de types, ce qui est crucial dans les bases de code vastes et complexes comme celle de Grafana.

10. Appliquer un style de code et une convention de nommage cohérents.

Imposez un formatage uniforme, des conventions de nommage et des structures de fichiers cohérentes à travers l'ensemble du codebase.

Non conforme : (styles mixtes)

const ApiData = await getdata();   // PascalCase for variable? function name not camelCase.
function Fetch_User() { ... }      // Unusual naming.

Conforme :

const apiData = await fetchData();
function fetchUser() { ... }

Pourquoi c'est important : Un style et une nomenclature cohérents améliorent la lisibilité et facilitent la compréhension et la maintenance du code par plusieurs contributeurs. Cela réduit la charge cognitive lors de la navigation dans le projet, prévient les bugs subtils causés par des malentendus, et garantit que les outils automatisés (linters, formatters, réviseurs de code) peuvent appliquer de manière fiable les normes de qualité dans un environnement de grande équipe.

Conclusion

Chaque règle ci-dessus répond à un défi récurrent dans la base de code de Grafana. Les appliquer de manière cohérente lors des revues de code aide l'équipe à maintenir un code propre et prévisible, à améliorer la sécurité en prévenant les vulnérabilités courantes et à faciliter l'intégration en fournissant des modèles clairs aux nouveaux contributeurs. À mesure que le projet évolue, ces pratiques maintiennent la base de code fiable, maintenable et plus facile à naviguer pour toutes les personnes impliquées. Suivre ces règles peut aider toute équipe d'ingénierie à construire et à maintenir des logiciels de haute qualité à grande échelle.

FAQ

Des questions ?

Pourquoi analyser le dépôt de Grafana pour les règles de revue de code ?

Grafana est un projet open-source vaste et mature, comptant des milliers de contributeurs. L'étude de sa base de code révèle des modèles d'ingénierie concrets qui aident les équipes à maintenir des logiciels propres, évolutifs et sécurisés à grande échelle.

Qu'est-ce qui différencie ces règles des vérifications de linting ou de formatage habituelles ?

Les linters traditionnels détectent les problèmes de syntaxe et de formatage. Ces règles basées sur Grafana vont plus loin, en se concentrant sur l'architecture, la lisibilité, la cohérence et les décisions de sécurité qui nécessitent une compréhension contextuelle — ce que les revues basées sur l'IA peuvent gérer.

Comment les outils basés sur l'IA peuvent-ils aider à détecter ces problèmes de qualité du code ?

Les outils d'IA peuvent analyser l'intention, le nommage, l'architecture et le contexte — pas seulement la syntaxe. Ils peuvent identifier les problèmes de maintenabilité, les abstractions peu claires et les problèmes de sécurité potentiels que l'analyse statique traditionnelle manque souvent.

Ces règles sont-elles spécifiques à Grafana ou peuvent-elles être utilisées par n'importe quelle équipe ?

Bien qu'ils soient inspirés de la base de code de Grafana, les principes s'appliquent à tout projet logiciel à grande échelle. Les équipes peuvent les adapter à leurs propres dépôts pour maintenir la cohérence, prévenir les régressions et améliorer l'intégration des nouveaux membres.

Quel est le rapport entre ces règles et les contrôles de qualité du code Aikido?

Chaque règle peut être mise en œuvre en tant que règle IA personnalisée dans la plateforme de qualité du code Aikido, permettant ainsi la détection automatisée des problèmes d'architecture, de lisibilité et de sécurité dans chaque pull request.

Sécurisez-vous maintenant.

Sécuriser votre code, votre cloud et votre runtime dans un système centralisé unique.
Détectez et corrigez les vulnérabilités rapidement et automatiquement.

Pas de carte de crédit requise | Résultats du scan en 32 secondes.