[{"data":1,"prerenderedAt":28},["ShallowReactive",2],{"articolo-logging-in-flutter-dal-print-ai-log-strutturati-in-produzione":3,"comments-article-logging-in-flutter-dal-print-ai-log-strutturati-in-produzione":27},{"id":4,"title":5,"slug":6,"excerpt":7,"body":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"status":16,"published_at":17,"meta_title":18,"meta_description":19,"category":20,"author":24},115,"Logging in Flutter: dal print ai log strutturati in produzione","logging-in-flutter-dal-print-ai-log-strutturati-in-produzione","Smettere di usare print() è il primo passo per avere app diagnosticabili. Guida pratica a dart:developer, al package logger, ai log strutturati, alla redazione dei dati sensibili, alla cattura degli errori globali e all'invio dei log a Crashlytics o Sentry.","## Perché il `print()` non basta\n\nQuasi ogni progetto Flutter inizia con qualche `print()` sparso nel codice. Funziona finché l'app gira sul tuo emulatore, ma smette di essere utile nel momento esatto in cui serve davvero: quando un utente segnala un bug che tu non riesci a riprodurre.\n\nI problemi concreti di `print()` sono tre:\n\n- **Finisce in produzione.** Le chiamate a `print()` non vengono rimosse dalla build di release: restano nei log di sistema, dove chiunque con un cavo USB e `adb logcat` può leggerle. Se stai stampando token, email o payload di risposta, hai appena creato una falla di sicurezza.\n- **Viene troncato.** Su Android il canale di log tronca le righe lunghe: stampare un JSON di 5 KB produce output incompleto. `debugPrint` mitiga il problema accodando le righe, ma non risolve nulla sul piano strutturale.\n- **Non ha livelli né contesto.** Non puoi filtrare, non sai da quale modulo arriva il messaggio, non hai timestamp affidabili e non puoi inoltrare nulla a un sistema di crash reporting.\n\nUn sistema di logging serio in Flutter si costruisce su quattro pilastri: **livelli**, **contesto**, **destinazioni (output)** e **redazione dei dati sensibili**. Vediamoli uno a uno.\n\n## `dart:developer`: la base della piattaforma\n\nPrima di installare qualsiasi pacchetto vale la pena conoscere `dart:developer`, incluso nell'SDK. La funzione `log()` scrive sul service protocol di Dart, quindi i messaggi compaiono in modo strutturato nel pannello **Logging** di DevTools, con nome, livello, errore e stack trace separati.\n\n```dart\nimport 'dart:developer' as developer;\n\ndeveloper.log(\n  'Login completato',\n  name: 'auth',\n  level: 800, \u002F\u002F INFO, secondo la scala di package:logging\n  time: DateTime.now(),\n);\n\ntry {\n  await repository.fetchProfile();\n} catch (error, stackTrace) {\n  developer.log(\n    'Fetch profilo fallito',\n    name: 'profile',\n    level: 1000, \u002F\u002F SEVERE\n    error: error,\n    stackTrace: stackTrace,\n  );\n}\n```\n\nI livelli seguono la convenzione di `package:logging`: `FINE` 500, `INFO` 800, `WARNING` 900, `SEVERE` 1000. Il vantaggio rispetto a `print()` è che DevTools può filtrare per `name` e livello; lo svantaggio è che non c'è formattazione, non c'è filtro automatico in release e non puoi inoltrare i messaggi altrove senza scriverti l'infrastruttura.\n\n## Il package `logger`: formattazione e livelli pronti all'uso\n\n[`logger`](https:\u002F\u002Fpub.dev\u002Fpackages\u002Flogger) è la scelta più diffusa per lo sviluppo quotidiano. Offre livelli, printer configurabili, filtri e output sostituibili.\n\n```yaml\ndependencies:\n  logger: ^2.4.0\n```\n\n```dart\nimport 'package:flutter\u002Ffoundation.dart';\nimport 'package:logger\u002Flogger.dart';\n\nfinal logger = Logger(\n  level: kReleaseMode ? Level.warning : Level.debug,\n  filter: ProductionFilter(),\n  printer: PrettyPrinter(\n    methodCount: 0,        \u002F\u002F niente stack trace per i log normali\n    errorMethodCount: 8,   \u002F\u002F stack completo quando c'è un errore\n    colors: !kReleaseMode,\n    printEmojis: !kReleaseMode,\n    dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart,\n  ),\n);\n```\n\nDue dettagli importanti:\n\n- Il filtro di default (`DevelopmentFilter`) **silenzia tutto in release**. Se vuoi che warning ed errori continuino a essere processati — per inoltrarli a un servizio remoto — devi usare `ProductionFilter`, che rispetta solo la soglia `level`.\n- Dalla versione 2.x il parametro `printTime` è stato sostituito da `dateTimeFormat`: se segui tutorial vecchi otterrai un errore di compilazione.\n\nL'uso è immediato:\n\n```dart\nlogger.t('Trace: costruzione widget');\nlogger.d('Carico ${items.length} elementi dalla cache');\nlogger.i('Sincronizzazione completata');\nlogger.w('Risposta API lenta: ${elapsed.inMilliseconds} ms');\nlogger.e('Checkout fallito', error: error, stackTrace: stackTrace);\n```\n\n## Incapsula il logger dietro una facciata\n\nChiamare `logger` direttamente da ogni file lega tutto il progetto a una libreria specifica. Meglio definire un'interfaccia tua: potrai cambiare implementazione, sostituire il logger nei test o aggiungere destinazioni senza toccare centinaia di import.\n\n```dart\nabstract interface class AppLogger {\n  void debug(String message, {Map\u003CString, Object?> context});\n  void info(String message, {Map\u003CString, Object?> context});\n  void warning(String message, {Object? error, StackTrace? stackTrace});\n  void error(String message, {Object? error, StackTrace? stackTrace});\n}\n\nclass LoggerAdapter implements AppLogger {\n  LoggerAdapter(this._logger, {required this.scope});\n\n  final Logger _logger;\n  final String scope;\n\n  String _format(String message, Map\u003CString, Object?> context) {\n    if (context.isEmpty) return '[$scope] $message';\n    final pairs = context.entries.map((e) => '${e.key}=${e.value}').join(' ');\n    return '[$scope] $message | $pairs';\n  }\n\n  @override\n  void debug(String message, {Map\u003CString, Object?> context = const {}}) =>\n      _logger.d(_format(message, context));\n\n  @override\n  void info(String message, {Map\u003CString, Object?> context = const {}}) =>\n      _logger.i(_format(message, context));\n\n  @override\n  void warning(String message, {Object? error, StackTrace? stackTrace}) =>\n      _logger.w('[$scope] $message', error: error, stackTrace: stackTrace);\n\n  @override\n  void error(String message, {Object? error, StackTrace? stackTrace}) =>\n      _logger.e('[$scope] $message', error: error, stackTrace: stackTrace);\n}\n```\n\nIl campo `scope` è il contesto minimo indispensabile: `auth`, `sync`, `checkout`. Quando apri DevTools con 400 righe di log, poter cercare `[sync]` fa la differenza tra cinque minuti e un'ora di debug.\n\n## Log strutturati: pensare in chiave\u002Fvalore, non in frasi\n\nUna riga come `Errore durante il salvataggio` è inutile. Una riga come:\n\n```\n[sync] upload fallito | entity=order id=8821 attempt=3 status=503 elapsedMs=1840\n```\n\nti dice cosa è successo, a quale risorsa, dopo quanti tentativi e quanto è durato. La regola pratica è: **il messaggio descrive l'evento, il contesto porta i dati**. Non concatenare valori nel testo in modo libero, passali come mappa.\n\nIn release, se inoltri i log a un backend, conviene serializzarli in JSON:\n\n```dart\nclass JsonPrinter extends LogPrinter {\n  @override\n  List\u003CString> log(LogEvent event) {\n    final payload = \u003CString, Object?>{\n      'ts': DateTime.now().toUtc().toIso8601String(),\n      'level': event.level.name,\n      'msg': event.message.toString(),\n      if (event.error != null) 'error': event.error.toString(),\n    };\n    return [jsonEncode(payload)];\n  }\n}\n```\n\n## Redigere i dati sensibili\n\nQuesto è il punto che la maggior parte dei progetti sbaglia. Token, password, numeri di carta, codici fiscali e indirizzi email non devono mai finire nei log, nemmeno in debug: le build di debug circolano fra colleghi, tester e macchine CI.\n\nUn piccolo helper di sanificazione risolve il 90% dei casi:\n\n```dart\nconst _sensitiveKeys = {\n  'password', 'token', 'access_token', 'refresh_token',\n  'authorization', 'pin', 'iban', 'card_number',\n};\n\nMap\u003CString, Object?> redact(Map\u003CString, Object?> input) {\n  return input.map((key, value) {\n    if (_sensitiveKeys.contains(key.toLowerCase())) {\n      return MapEntry(key, '***');\n    }\n    if (value is Map\u003CString, Object?>) return MapEntry(key, redact(value));\n    return MapEntry(key, value);\n  });\n}\n\nString maskEmail(String email) {\n  final parts = email.split('@');\n  if (parts.length != 2 || parts.first.isEmpty) return '***';\n  return '${parts.first[0]}***@${parts.last}';\n}\n```\n\nE applicalo nell'interceptor HTTP, dove il rischio è più alto:\n\n```dart\nclass LoggingInterceptor extends Interceptor {\n  LoggingInterceptor(this._log);\n  final AppLogger _log;\n\n  @override\n  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {\n    _log.debug('HTTP →', context: {\n      'method': options.method,\n      'path': options.path,\n      'headers': redact(Map\u003CString, Object?>.from(options.headers)),\n    });\n    handler.next(options);\n  }\n\n  @override\n  void onResponse(Response response, ResponseInterceptorHandler handler) {\n    _log.debug('HTTP ←', context: {\n      'path': response.requestOptions.path,\n      'status': response.statusCode,\n      'bytes': response.data is String ? (response.data as String).length : null,\n    });\n    handler.next(response);\n  }\n\n  @override\n  void onError(DioException err, ErrorInterceptorHandler handler) {\n    _log.warning(\n      'HTTP errore ${err.requestOptions.path}',\n      error: err,\n    );\n    handler.next(err);\n  }\n}\n```\n\nNota che il body della risposta **non** viene loggato per intero: ne registriamo solo la dimensione. Loggare interi payload è il modo più rapido per riempire i log di dati personali.\n\n## Inoltrare i log al crash reporting\n\nI log diventano davvero preziosi quando accompagnano un crash. Sia Crashlytics sia Sentry supportano il concetto di *breadcrumb*: una coda di eventi recenti allegata automaticamente al report.\n\nCon `logger` basta scrivere un `LogOutput` personalizzato e comporlo con quello di console:\n\n```dart\nclass CrashlyticsOutput extends LogOutput {\n  @override\n  void output(OutputEvent event) {\n    final crashlytics = FirebaseCrashlytics.instance;\n    for (final line in event.lines) {\n      crashlytics.log(line); \u002F\u002F breadcrumb\n    }\n    if (event.level.index >= Level.error.index) {\n      crashlytics.recordError(\n        event.origin.error ?? event.origin.message,\n        event.origin.stackTrace,\n        fatal: false,\n      );\n    }\n  }\n}\n\nfinal logger = Logger(\n  filter: ProductionFilter(),\n  printer: kReleaseMode ? JsonPrinter() : PrettyPrinter(methodCount: 0),\n  output: MultiOutput([\n    ConsoleOutput(),\n    if (kReleaseMode) CrashlyticsOutput(),\n  ]),\n);\n```\n\nCon Sentry l'equivalente è `Sentry.addBreadcrumb(Breadcrumb(message: line, level: ...))`.\n\n## Catturare tutto: errori Flutter, errori async e isolate\n\nUn sistema di logging incompleto perde proprio gli errori più importanti. Servono tre agganci distinti:\n\n```dart\nvoid main() {\n  WidgetsFlutterBinding.ensureInitialized();\n\n  \u002F\u002F 1. Errori nel framework (build, layout, paint)\n  FlutterError.onError = (details) {\n    logger.e('FlutterError', error: details.exception, stackTrace: details.stack);\n    FlutterError.presentError(details);\n  };\n\n  \u002F\u002F 2. Errori asincroni non gestiti nell'isolate principale\n  PlatformDispatcher.instance.onError = (error, stack) {\n    logger.e('Uncaught async error', error: error, stackTrace: stack);\n    return true; \u002F\u002F errore gestito\n  };\n\n  runApp(const MyApp());\n}\n```\n\n`PlatformDispatcher.instance.onError` (disponibile da Flutter 3.3) ha sostituito nella pratica il vecchio pattern con `runZonedGuarded`: è più semplice e non interferisce con il binding. Se usi `Isolate.spawn` ricorda che gli isolate secondari hanno una gestione errori separata: registra un `ReceivePort` con `Isolate.current.addErrorListener` dentro l'isolate oppure passa `onError` allo spawn.\n\n## Scrivere i log su file (e ruotarli)\n\nPer le app che devono funzionare offline o in contesti industriali, un file locale consente all'utente di esportare la diagnostica. Il punto critico è la **rotazione**: senza limiti, un log su file riempie lo storage del dispositivo.\n\n```dart\nclass RotatingFileOutput extends LogOutput {\n  RotatingFileOutput(this.file, {this.maxBytes = 512 * 1024});\n\n  final File file;\n  final int maxBytes;\n  IOSink? _sink;\n\n  @override\n  Future\u003Cvoid> init() async {\n    if (await file.exists() && await file.length() > maxBytes) {\n      final backup = File('${file.path}.1');\n      if (await backup.exists()) await backup.delete();\n      await file.rename(backup.path);\n    }\n    _sink = file.openWrite(mode: FileMode.append);\n  }\n\n  @override\n  void output(OutputEvent event) {\n    for (final line in event.lines) {\n      _sink?.writeln(line);\n    }\n  }\n\n  @override\n  Future\u003Cvoid> destroy() async {\n    await _sink?.flush();\n    await _sink?.close();\n  }\n}\n```\n\nIl file va creato in `getApplicationSupportDirectory()` (non in `getApplicationDocumentsDirectory()`, che su iOS può finire nel backup iCloud e nell'app Files) e l'esportazione si gestisce con `share_plus`.\n\n## Testare il logging\n\nAvendo una facciata `AppLogger`, nei test puoi iniettare un'implementazione che accumula i messaggi in memoria e verificare che gli eventi critici vengano registrati:\n\n```dart\nclass InMemoryLogger implements AppLogger {\n  final List\u003CString> messages = [];\n  @override\n  void error(String message, {Object? error, StackTrace? stackTrace}) =>\n      messages.add('ERROR $message');\n  \u002F\u002F ... altri metodi\n}\n\ntest('registra un errore quando il salvataggio fallisce', () async {\n  final log = InMemoryLogger();\n  final repo = OrderRepository(api: FailingApi(), logger: log);\n\n  await repo.save(order);\n\n  expect(log.messages, contains(startsWith('ERROR')));\n});\n```\n\n## Checklist finale\n\n- Vieta `print()` nel progetto: aggiungi `avoid_print: error` in `analysis_options.yaml`.\n- Usa livelli coerenti: `debug` per lo sviluppo, `info` per gli eventi di business, `warning` per le anomalie recuperabili, `error` solo per ciò che richiede un intervento.\n- Dai sempre uno **scope** ai log e passa i dati come coppie chiave\u002Fvalore.\n- Redigi header, token e dati personali **prima** di scrivere la riga, non dopo.\n- In release alza la soglia ad almeno `warning` e inoltra tutto a Crashlytics o Sentry.\n- Non loggare dentro `build()` o in callback ad alta frequenza come `onScroll`: la serializzazione delle stringhe costa e introduce jank.\n- Se scrivi su file, imposta un limite di dimensione e una rotazione.\n\nUn logging ben progettato non si nota finché non serve. Quando serve, è la differenza tra riprodurre un bug in dieci minuti e chiudere il ticket con un \"non riproducibile\".","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002Fdf6f9dad-5e0f-4f40-bf7a-1e1e1b953fa9.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1726568313407-c7d9c8a8ce88?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTA5MTM2OTR8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"HackerNoon","https:\u002F\u002Funsplash.com\u002F@hackernoon","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-man-sitting-in-front-of-a-laptop-computer-tvYJNqq0I6A",null,"published","2026-10-02T04:01:34+00:00","Logging in Flutter: guida pratica ai log strutturati","Come sostituire print() con un sistema di logging serio in Flutter: livelli, log strutturati, redazione dei dati sensibili e invio a Crashlytics o Sentry.",{"id":21,"name":22,"slug":23},3,"Best practice","best-practice",{"id":25,"name":26},1,"Flutter Bot",[],1792316907979]