SnapLogs
Retour au blog
Mobile

Snap Logger : le guide du logging moderne pour apps mobile

Le logging mobile a changé. Fini les NSLog et Logcat bruts : en 2026, un bon logger mobile gère des niveaux, structure ses messages, redacte les données sensibles, sample en production et pousse les logs vers un backend centralisé. Ce guide détaillé compare les systèmes de logging natifs (NSLog, os_log, Logcat, Timber, flogger), explique le remote logging, et montre comment intégrer l'API SnapLogs dans Flutter, React Native, iOS et Android avec des exemples de code concrets.

S

SnapLogs Team

Équipe produit

Snap Logger : le guide du logging moderne pour apps mobile

Snap Logger : le guide du logging moderne pour apps mobile

Le logging est le plus vieux réflexe du développeur, et pourtant il reste mal fait dans 90% des apps mobile. NSLog verbeux, Logcat sans structure, logs qui disparaissent après redémarrage, données sensibles en clair : les anti-patterns sont légion. Ce guide vous donne le socle moderne du logging mobile en 2026, compare les systèmes natifs, et montre comment intégrer SnapLogs comme couche de remote logging universelle.

Pourquoi le logging mobile moderne est critique

Le logging mobile sert trois objectifs distincts :

  • Debug local : comprendre ce qui se passe pendant le dev.
  • Debug production : diagnostiquer les bugs remontés par les utilisateurs.
  • Observabilité produit : mesurer les flux, les conversions, les churns.

Historiquement, on confondait ces trois objectifs avec un seul outil (NSLog). Aujourd'hui, on les sépare :

  • Dev local : debugPrint (Flutter), os_log (iOS), Log.d (Android).
  • Production : remote logging via SnapLogs, Sentry, Bugsnag.
  • Produit : analytics (Amplitude, Mixpanel).

Cet article se concentre sur le debug production, le plus mal couvert aujourd'hui.

Les composants d'un logger mobile moderne

Un logger mobile moderne doit gérer six dimensions.

1. Les niveaux de log

Cinq niveaux standards :

| Niveau | Usage | En production | | --- | --- | --- | | debug | Dev, traces internes | Désactivé | | verbose | Détails très fins | Désactivé | | info | Événements métier | Activé | | warning | Anomalies récupérables | Activé | | error | Exceptions catchées | Activé | | fatal | Crashes imminents | Activé |

L'erreur classique est de tout logger en info. Résultat : le bruit masque le signal. SnapLogs recommande :

  • info : événements métier (login, payment, navigation).
  • warning : comportement inhabituel mais géré (retry, fallback).
  • error : exception catchée mais inattendue.

2. Le structured logging

Le structured logging remplace le texte libre par des paires clé-valeur.

Texte libre :

User 42 paid 49.99 EUR with card

Structuré :

{
  "level": "info",
  "message": "payment_done",
  "fields": {
    "user_id": 42,
    "amount": 49.99,
    "currency": "EUR",
    "method": "card"
  },
  "timestamp": "2026-08-09T11:03:00Z"
}

Le structuré permet :

  • Le filtrage par champ côté backend (tous les paiements > 100€).
  • L'agrégation (count par method).
  • La corrélation avec d'autres événements (login juste avant).

SnapLogs impose le structuré. Voyez la documentation de l'API.

3. La redaction

Logger du PII (Personally Identifiable Information) est une violation RGPD. Un bon logger redacte par regex :

  • Email : \b[\w.]+@[\w.]+\b
  • IBAN : \b[A-Z]{2}\d{2}[A-Z0-9]{12,30}\b
  • Carte bancaire : \b\d{13,16}\b
  • Token JWT : \beyJ[\w-]+\.[\w-]+\.[\w-]+\b

SnapLogs intègre la redaction nativement :

await SnapLogs.init(
  apiKey: const String.fromEnvironment('SNAPLOGS_KEY'),
  redactPatterns: [
    r'\b[\w.]+@[\w.]+\b',
    r'\b[A-Z]{2}\d{2}[A-Z0-9]{12,30}\b',
    r'\b\d{13,16}\b',
  ],
);

Tout log passé par SnapLogs est automatiquement filtré.

4. Le sampling

Logger 100% des events en production tue la batterie et le forfait data. On sample :

  • error : 100% (jamais samplé).
  • warning : 100% (volume bas).
  • info : 1% à 10% selon le volume.
  • debug : 0% en prod.
await SnapLogs.init(
  apiKey: key,
  sampleRate: 0.05, // 5% des info logs
  errorSampleRate: 1.0, // 100% des errors
);

5. Le batching et l'envoi réseau

Un log par requête HTTP est prohibitif. On batch :

  • Buffer local de 50 à 200 logs.
  • Flush toutes les 30 secondes ou quand le buffer est plein.
  • Retry avec backoff exponentiel (1s, 2s, 4s, 8s).
  • File d'attente persistante (SQLite) pour la hors-ligne.

SnapLogs gère tout cela côté SDK. Vous ne voyez que l'API log().

6. La corrélation avec les crashs

Un log sans crash est utile. Un log juste avant un crash est crucial. SnapLogs attache automatiquement les 50 derniers logs à un rapport de crash. Vous obtenez la timeline complète du bug.

Comparaison des log systems natifs

Voyons ce que proposent les natifs, et pourquoi ils ne suffisent plus.

iOS : NSLog vs os_log vs Logger

NSLog est l'ancêtre. Verbeux, non structuré, sync. À éviter.

NSLog("User \(userId) paid \(amount)")

os_log (iOS 10+) est le remplaçant. Structuré, async, performant.

import os

let log = OSLog(subsystem: "pro.snaplogs.demo", category: "payment")
os_log("User %{public}d paid %{public}.2f", log: log, type: .info, userId, amount)

Logger (iOS 14+) est l'API Swift moderne :

import os

let logger = Logger(subsystem: "pro.snaplogs.demo", category: "payment")
logger.info("User \(userId) paid \(amount)")

Limite majeure : os_log et Logger écrivent dans le système, pas dans un backend distant. Pour le debug production, il faut ajouter un remote logger.

Android : Logcat vs Timber vs flogger

Logcat est l'API brute :

import android.util.Log

Log.d("Payment", "User $userId paid $amount")

Verbeux, non structuré, disparaît au redémarrage.

Timber (Jake Wharton) est le standard de fait :

import timber.log.Timber

Timber.plant(Timber.DebugTree())

Timber.i("User %d paid %.2f", userId, amount)
Timber.e(exception, "Payment failed for user %d", userId)

Timber plante des arbres (tree pattern). On peut ajouter un SnapLogsTree :

class SnapLogsTree : Timber.Tree() {
  override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
    val level = when (priority) {
      Log.DEBUG -> LogLevel.debug
      Log.INFO -> LogLevel.info
      Log.WARN -> LogLevel.warning
      Log.ERROR -> LogLevel.error
      else -> LogLevel.info
    }
    SnapLogs.log(level = level, message = message, throwable = t)
  }
}

Timber.plant(SnapLogsTree())

flogger (Google) est une alternative Java-style, moins idiomatique Kotlin.

Flutter : debugPrint vs developer.log

debugPrint est simple :

debugPrint('User $userId paid $amount');

Mais il disparaît en release mode et n'est pas structuré.

dart:developer est plus riche :

import 'dart:developer' as developer;

developer.log(
  'payment_done',
  name: 'payment',
  error: exception,
);

Mais il reste local. Pour la prod, il faut SnapLogs :

SnapLogs.log(
  level: LogLevel.info,
  message: 'payment_done',
  fields: {'user_id': userId, 'amount': amount},
);

React Native : console.log vs react-native-logs

console.log fonctionne en dev mais disparaît en prod bundle.

react-native-logs est une lib communautaire. On peut y brancher SnapLogs :

import logger from 'react-native-logs';
import { SnapLogsTransport } from 'snaplogs-react-native';

const log = logger.createLogger({
  transport: SnapLogsTransport,
  transportOptions: { apiKey: process.env.SNAPLOGS_KEY },
  levels: { debug: 0, info: 1, warn: 2, error: 3 },
});

log.info('payment_done', { user_id: 42, amount: 49.99 });

Remote logging : pourquoi c'est le game changer

Le remote logging résout trois problèmes :

  • Disparition au redémarrage : les logs natifs sont perdus. Le remote les persiste.
  • Pas d'accès au device : vous ne pouvez pas demander à l'utilisateur de brancher son téléphone.
  • Corrélation : un backend centralisé permet de croiser les logs de tous les users.

Sans remote logging, vous debugguez à l'aveugle pour 99% des bugs signalés par les utilisateurs. C'est pour cela que le logging natif seul ne suffit plus.

Volume de logs par niveau DEBUG/INFO sur 24h

Le volume de logs varie selon l'heure et le traffic. Le sampling et le batching evitent la surcharge tout en capturant les pics d'activite.

L'API SnapLogs log ingestion

SnapLogs expose une API REST v1 pour l'ingestion de logs. Vous pouvez l'appeler directement ou via le SDK.

Endpoint

POST https://api.snaplogs.pro/v1/logs
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY

Payload

{
  "logs": [
    {
      "level": "info",
      "message": "payment_done",
      "fields": {
        "user_id": 42,
        "amount": 49.99,
        "currency": "EUR",
        "method": "card"
      },
      "timestamp": "2026-08-09T11:03:00.123Z",
      "session_id": "sess_abc123"
    }
  ]
}

Réponse

{
  "accepted": 1,
  "rejected": 0
}

Les logs sont indexés et recherchables dans le dashboard SnapLogs. Vous pouvez les attacher à un rapport de bug, les exporter vers BigQuery, ou les streamer vers Slack via webhook.

Exemples d'intégration SnapLogs

Flutter

import 'package:flutter/material.dart';
import 'package:snaplogs/snaplogs.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await SnapLogs.init(
    apiKey: const String.fromEnvironment('SNAPLOGS_KEY'),
    environment: 'production',
    sampleRate: 0.05,
    errorSampleRate: 1.0,
    redactPatterns: [r'\b[\w.]+@[\w.]+\b', r'\b\d{16}\b'],
  );

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: ElevatedButton(
            onPressed: () async {
              try {
                await chargeCustomer(amount: 49.99);
                SnapLogs.log(
                  level: LogLevel.info,
                  message: 'payment_done',
                  fields: {'amount': 49.99, 'currency': 'EUR'},
                );
              } catch (e, st) {
                SnapLogs.log(
                  level: LogLevel.error,
                  message: 'payment_failed',
                  fields: {'amount': 49.99},
                  error: e,
                  stackTrace: st,
                );
              }
            },
            child: const Text('Pay 49.99 EUR'),
          ),
        ),
      ),
    );
  }
}

Future<void> chargeCustomer({required double amount}) async {
  // ...
}

React Native

import { SnapLogs } from 'snaplogs-react-native';

SnapLogs.init({
  apiKey: process.env.SNAPLOGS_KEY,
  sampleRate: 0.05,
  redactPatterns: [/[\w.]+@[\w.]+/, /\b\d{16}\b/],
});

// Usage
async function pay(amount) {
  try {
    await api.post('/charge', { amount });
    SnapLogs.log({
      level: 'info',
      message: 'payment_done',
      fields: { amount, currency: 'EUR' },
    });
  } catch (error) {
    SnapLogs.log({
      level: 'error',
      message: 'payment_failed',
      fields: { amount },
      error,
    });
  }
}

iOS (Swift)

import SnapLogs

@main
struct MyApp: App {
  init() {
    SnapLogs.configure(
      apiKey: "YOUR_KEY",
      sampleRate: 0.05,
      redactPatterns: [#"[\w.]+@[\w.]+"#, #"\b\d{16}\b"#]
    )
  }

  var body: some Scene {
    WindowGroup {
      ContentView()
    }
  }
}

// Usage
func pay(amount: Double) {
  Task {
    do {
      try await api.charge(amount: amount)
      SnapLogs.log(level: .info, message: "payment_done", fields: ["amount": amount])
    } catch {
      SnapLogs.log(level: .error, message: "payment_failed", fields: ["amount": amount], error: error)
    }
  }
}

Android (Kotlin)

import pro.snaplogs.sdk.SnapLogs

class MyApp : Application() {
  override fun onCreate() {
    super.onCreate()
    SnapLogs.init(
      apiKey = "YOUR_KEY",
      sampleRate = 0.05,
      redactPatterns = listOf(Regex("[\\w.]+@[\\w.]+"), Regex("\\b\\d{16}\\b"))
    )
  }
}

// Usage
suspend fun pay(amount: Double) {
  try {
    api.charge(amount)
    SnapLogs.log(level = LogLevel.INFO, message = "payment_done", fields = mapOf("amount" to amount))
  } catch (e: Exception) {
    SnapLogs.log(level = LogLevel.ERROR, message = "payment_failed", fields = mapOf("amount" to amount), error = e)
  }
}

Best practices du logging mobile

1. Toujours logger avec un niveau

N'utilisez jamais log(message) sans niveau. Précisez info, warning, error. Cela permet le filtrage côté backend.

2. Structurer les messages

Préférez payment_done avec fields {amount: 49.99} à User 42 paid 49.99 EUR with card. Le premier est filtrable, le second est du bruit.

3. Ne jamais logger de PII

Même avec redaction, évitez de logger :

  • Email
  • Numéro de téléphone
  • Token JWT
  • IBAN
  • Coordonnées GPS précises

SnapLogs redact par regex, mais la meilleure redaction est celle qu'on n'écrit pas.

4. Sampler en production

Sur une app à 100k DAU, logger 100% des events génère 10M logs/jour. C'est inutile et coûteux. Samplez à 1-5% sur les info, gardez 100% sur error.

5. Batcher les envois

Un log par requête HTTP est prohibitif. SnapLogs batch automatiquement (50-200 logs par flush). Si vous appelez l'API directement, batchez côté client.

6. Attacher un session_id

Un session_id permet de suivre le parcours utilisateur. Ajoutez-le à tous les logs de la session :

final sessionId = SnapLogs.startSession();
// tous les logs suivants incluent session_id automatiquement

7. Gérer la hors-ligne

Le SDK SnapLogs met en file d'attente les logs hors ligne et les envoie dès la connexion revient. Si vous appelez l'API directement, persistez en SQLite.

8. Versionner le schema

Ajoutez schemaVersion dans les fields pour pouvoir migrer sans casser le parsing backend :

SnapLogs.log(
  level: LogLevel.info,
  message: 'payment_done',
  fields: {'amount': 49.99, 'schemaVersion': 2},
);

Pièges à éviter

1. Logger en boucle

Un log dans un build() Flutter peut générer 1000 logs/seconde. Ne loggez jamais dans un build. Loggez dans des événements (tap, navigation, callback).

2. Logger des exceptions sans contexte

// Mal
catch (e) { SnapLogs.log(level: LogLevel.error, message: e.toString()); }

// Bien
catch (e, st) {
  SnapLogs.log(
    level: LogLevel.error,
    message: 'payment_failed',
    fields: {'amount': amount, 'method': method},
    error: e,
    stackTrace: st,
  );
}

3. Confondre analytics et logging

L'analytics mesure le produit (conversion, funnel). Le logging debug le code (erreurs, flows). Ne mélangez pas : SnapLogs pour le debug, Amplitude pour le produit.

4. Oublier de tester le logger

Testez que vos logs partent en release mode. Le debug mode peut masquer des erreurs (permissions réseau différentes).

flutter run --release

Architecture d'un système de remote logging

Un remote logger moderne repose sur quatre couches.

Couche 1 : le SDK client

Le SDK tourne dans l'app. Il expose log(), captureException(), addBreadcrumb(). Il gère le buffering, le sampling, la redaction, et l'envoi.

Caractéristiques attendues :

  • Thread-safe : pas de race condition sur le buffer.
  • Non-bloquant : un log ne doit jamais geler l'UI.
  • Persistant : les logs survivent à un kill de l'app.
  • Configurable à distance : on peut changer le sampleRate sans redeploy.

Couche 2 : le transport

Le transport envoie les logs au backend. Trois options :

  • HTTP POST batch : simple, universel.
  • WebSocket : temps réel, plus cher.
  • gRPC : performant, plus complexe.

SnapLogs utilise HTTP POST batch par défaut, avec une option WebSocket pour le temps réel.

Couche 3 : le backend d'ingestion

Le backend reçoit, valide, indexe, et stocke. Exigences :

  • Throughput : >10k logs/seconde par tenant.
  • Schema flexible : les fields sont libres (JSON).
  • Indexation : par level, message, session_id, timestamp.
  • Rétention : 30 à 90 jours par défaut.

SnapLogs indexe sur Elasticsearch managé. La recherche est全文.

Couche 4 : l'affichage et l'export

Les logs sont consultables dans le dashboard SnapLogs. Features :

  • Recherche par level, message, field.
  • Timeline par session_id.
  • Export vers BigQuery, S3, Slack webhook.

Cette architecture en 4 couches est ce qui distingue un vrai remote logger d'un simple console.log pushé en HTTP.

Logs structurés vs logs textuels : exemple complet

Voyons un cas concret : tracer un flow de paiement.

Version textuelle (à éviter)

print('Starting payment for user 42');
print('Amount: 49.99 EUR, method: card');
print('Calling provider...');
print('Provider returned 200');
print('Payment done');

Côté backend, vous recevez 5 lignes de texte. Pour les filtrer, il faut parser. Pour les corréler, il faut deviner. C'est inutilisable à grande échelle.

Version structurée (recommandée)

SnapLogs.log(level: LogLevel.info, message: 'payment_start', fields: {'user_id': 42, 'amount': 49.99, 'currency': 'EUR', 'method': 'card'});
SnapLogs.log(level: LogLevel.info, message: 'provider_call', fields: {'provider': 'stripe', 'endpoint': '/charges'});
SnapLogs.log(level: LogLevel.info, message: 'provider_response', fields: {'status': 200, 'duration_ms': 412});
SnapLogs.log(level: LogLevel.info, message: 'payment_done', fields: {'user_id': 42, 'amount': 49.99, 'schemaVersion': 2});

Côté backend, vous pouvez :

  • Filtrer tous les payment_done avec amount > 100.
  • Compter le temps moyen entre payment_start et payment_done.
  • Corréler un payment_failed avec le provider_response précédent.

Le structuré transforme les logs en données exploitables.

Logs et traces distribuées : corrélation multi-services

Le logging mobile prend tout son sens quand on le corrèle aux logs backend. Une erreur mobile « timeout » peut venir du backend, du CDN, ou du réseau opérateur. Sans corrélation, vous cherchez au mauvais endroit.

Trace ID et span ID

Le pattern standard : chaque requête mobile embarque un trace_id et un span_id :

import 'package:uuid/uuid.dart';

Future<void> tracedRequest(String url) async {
  final traceId = const Uuid().v4();
  final spanId = const Uuid().v4().substring(0, 16);

  SnapLogs.log(
    level: LogLevel.info,
    message: 'http_request_start',
    fields: {'trace_id': traceId, 'span_id': spanId, 'url': url},
  );

  final response = await http.post(
    Uri.parse(url),
    headers: {
      'X-Trace-Id': traceId,
      'X-Span-Id': spanId,
    },
    body: {...},
  );

  SnapLogs.log(
    level: LogLevel.info,
    message: 'http_request_end',
    fields: {
      'trace_id': traceId,
      'status': response.statusCode,
      'duration_ms': duration.inMilliseconds,
    },
  );
}

Côté backend (Node.js exemple) :

app.post('/charge', (req, res) => {
  const traceId = req.headers['x-trace-id'];
  logger.info({ message: 'charge_start', trace_id: traceId, user_id: req.body.user_id });

  // ... process ...

  logger.info({ message: 'charge_end', trace_id: traceId, status: 200 });
  res.json({ ok: true });
});

Le trace_id permet de suivre le parcours complet : tap mobile, requête HTTP, traitement backend, réponse. En cas de bug, vous recherchez le trace_id dans SnapLogs et vous avez toute la timeline.

SnapLogs trace explorer

Le dashboard SnapLogs propose un trace explorer qui agrège les logs par trace_id. Vous voyez :

11:03:01  mobile   http_request_start    trace=abc123  url=/charge
11:03:01  backend  charge_start          trace=abc123  user_id=42
11:03:02  backend  charge_end            trace=abc123  status=200
11:03:02  mobile   http_request_end      trace=abc123  status=200

Cette corrélation multi-services est ce qui transforme un simple logger en outil d'observabilité complet.

OpenTelemetry compatibility

SnapLogs supporte le format OpenTelemetry pour les trace_id et span_id. Vous pouvez exporter les traces vers Jaeger, Zipkin, ou Honeycomb. Le SDK SnapLogs ne remplace pas un APM complet, mais il s'intègre dans une stack existante.

Logs et analytics : la frontière

Une confusion fréquente : faut-il logger les événements produit (click, conversion) dans SnapLogs ou dans un analytics tool ?

La règle

  • Analytics (Amplitude, Mixpanel) : événements produit, funnels, conversion. Agregation, dashboards.
  • Logging (SnapLogs) : événements debug, erreurs, flows techniques. Recherche, corrélation, incident response.

Ne mélangez pas. Un log de debug dans Amplitude pollue les dashboards produit. Un event de conversion dans SnapLogs pollue les recherches de bugs.

Quand les deux se croisent

Parfois, un event est les deux. Exemple : payment_failed. C'est un event produit (conversion ratée) et un event debug (erreur technique). La solution : logger dans les deux, avec le même trace_id.

await api.post('/charge', body: {...}).catchError((e) async {
  await SnapLogs.log(
    level: LogLevel.error,
    message: 'payment_failed',
    fields: {'trace_id': traceId, 'reason': e.code},
  );
  await Amplitude.track('payment_failed', properties: {'reason': e.code, 'trace_id': traceId});
});

Le trace_id permet de croiser les deux systèmes.

Combinaison avec un crash reporter

Le logging prend tout son sens combiné à un crash reporter. SnapLogs attache automatiquement les 50 derniers logs à un rapport de crash. Si vous utilisez Sentry en parallèle :

await SentryFlutter.init(
  (options) {
    options.dsn = const String.fromEnvironment('SENTRY_DSN');
    options.beforeSend = (event) {
      // Envoie aussi à SnapLogs pour avoir les logs en contexte
      SnapLogs.log(
        level: LogLevel.error,
        message: 'sentry_event',
        fields: {'event_id': event.eventId.toString()},
      );
      return event;
    };
  },
  appRunner: () => runApp(const MyApp()),
);

Le crash remonte dans Sentry, les logs dans SnapLogs. Le ticket support contient les deux. Pour le détail, voir notre comparatif des bug trackers Flutter.

Conclusion

Le logging mobile moderne n'est plus une option en 2026. NSLog et Logcat bruts ne suffisent plus pour debugguer une app en production. Un logger moderne gère des niveaux, structure ses messages, redacte le PII, sample intelligemment, et pousse vers un backend centralisé. SnapLogs intègre toutes ces dimensions dans un seul SDK, pour Flutter, React Native, iOS et Android.

Pour aller plus loin :

Le logging n'est pas une étape de finition, c'est le socle de l'observabilité mobile. Sans lui, vous debugguez à l'aveugle.

#logs#debugging#sdk#errors#performance#flutter#react-native

Articles liés

Explorez la documentation

Guides complets, references API, exemples de code multiplateforme.

Lire la doc