Skip to content

Lokales App-Logging (Variante A): OpenTelemetry → JSON-Lines-Datei, Dev-Mode-UI zum Aktivieren/Einsehen/Teilen (Default: aus) #1639

@ManAnRuck

Description

@ManAnRuck

Kontext & Motivation

Wir wollen Logs lokal auf dem Gerät sammeln (Datenminimierung) und Nutzer:innen im Dev-Mode-Screen das Aktivieren, Einsehen und manuelle Teilen der Logs ermöglichen. Es werden keine Logs automatisch an Server gesendet. Das erfüllt das Prinzip der Datenminimierung (Art. 5(1)(c) DSGVO: „…angemessen, erheblich und auf das für die Zwecke notwendige Maß beschränkt…“). 

Entscheidung (Vorschlag)

Variante A: OpenTelemetry-Logs (JS) mit eigenem lokalen Exporter, der JSON-Lines (NDJSON) in rotierende Dateien schreibt. Vorteile:
• Standardisierte Log-API/Modell (später erweiterbar auf OTLP/HTTP, falls jemals gewünscht). 
• Einfaches, stream-freundliches Format (JSON-Lines) – robust für Datei-Append, leicht zu parsen/teilen. 
• Keine Netzwerkabhängigkeit, volle Offline-Funktionalität.

Scope

•	In-Scope
•	OTel-Logger-Pipeline (Provider → Batch-Processor → lokaler Datei-Exporter).  
•	Dev-Mode-Screen: Logging aktivieren/deaktivieren, anzeigen, Datei teilen.
•	Log-Rotation + Retention (Größe/Zeit).
•	Grundlegende PII-Redaktion/Whitelist.
•	Out-of-Scope
•	Server-Upload, Remote-Collectors, Alerting.

Architektur & Design

High-Level

App Code ──► LogService (Port)
           └── OTel Adapter (LoggerProvider + BatchProcessor)
                 └── FileLogExporter (JSONL via react-native-fs)
                       └── /AppData/Logs/*.jsonl (rotierend)
Dev-Mode-UI ─► toggelt LogService, liest Datei(en), Share-Sheet

Design-Prinzipien

•	Separation of Concerns: UI kennt nur LogService (Port). OTel + FS sind Adapter.
•	Dependency Inversion: App-Code loggt über LogService-Interface; OTel/FS austauschbar.
•	Feature Flag / Runtime-Toggle: Logging aus per Default; nur manuell aktivierbar im Dev-Mode-Screen.
•	Security & Privacy by Design: Whitelist-Attribute, PII-Redaktion, lokale Speicherung, kurze Retention; keine automatischen Exporte. OWASP-Guidelines gegen „Sensitive Data in Logs“.  

Technische Details

Libraries

•	OpenTelemetry Logs (JS): @opentelemetry/api-logs, @opentelemetry/sdk-logs.  
•	Filesystem: react-native-fs (oder gepflegte Forks, falls nötig).  
•	Teilen: Core-API Share (Text) und/oder react-native-share (Dateien/URLs).  

Dateipfade & Backup-Exklusion (iOS)

•	Logs unter …/Library/Application Support/Logs/ oder …/Documents/Logs/.
•	Bei iOS: Ordner mit NSURLIsExcludedFromBackupKey anlegen, um iCloud-Backup auszuschließen. (RNFS unterstützt Flag bei mkdir).  

Datenformat

•	JSON-Lines (.jsonl): ein LogRecord pro Zeile, robust für Append/Streaming.  
•	Optional spätere OTLP/HTTP JSON-Kompatibilität, falls Export gewünscht.  

UI/UX (Dev-Mode-Screen)

•	Schalter „Lokales Logging aktivieren“ (Default aus).
•	Liste mit letzten N Einträgen (Level, Zeit, Kurztext; Tap für Details).
•	Buttons: „Teilen“ (öffnet Share-Sheet mit Logdatei), „Löschen“, „Export als ZIP“ (optional).

Beispiel-Implementierung (Ausschnitt)

1) OTel Setup + Datei-Exporter (TypeScript)

// logging/otel.ts
import * as logsAPI from '@opentelemetry/api-logs';
import { LoggerProvider, BatchLogRecordProcessor, ReadableLogRecord, LogRecordExporter } from '@opentelemetry/sdk-logs';
import { Resource } from '@opentelemetry/resources';
import RNFS from 'react-native-fs';

const LOG_DIR = `${RNFS.LibraryDirectoryPath ?? RNFS.DocumentDirectoryPath}/Logs`;
const LOG_FILE = `${LOG_DIR}/app-logs.jsonl`;

export async function ensureLogDir() {
  await RNFS.mkdir(LOG_DIR, { NSURLIsExcludedFromBackupKey: true }); // iOS: exclude from iCloud backup
}

class FileLogExporter implements LogRecordExporter {
  constructor(private path: string) {}
  export(batch: ReadableLogRecord[], done: (r:{code:number}) => void) {
    const lines = batch.map(r => JSON.stringify({
      ts: Date.now(),
      level: r.severityText ?? 'INFO',
      body: typeof r.body === 'string' ? r.body : String(r.body ?? ''),
      attrs: r.attributes ?? {},
      traceId: r.spanContext?.().traceId,
      spanId: r.spanContext?.().spanId,
    })).join('\n') + '\n';

    RNFS.appendFile(this.path, lines, 'utf8').then(
      () => done({ code: 0 }),
      () => done({ code: 1 })
    );
  }
  async shutdown() {}
}

let provider: LoggerProvider | null = null;
export async function startLocalLogging() {
  await ensureLogDir();
  provider = new LoggerProvider({ resource: new Resource({ 'service.name': 'my-rn-app' }) });
  provider.addLogRecordProcessor(new BatchLogRecordProcessor(new FileLogExporter(LOG_FILE)));
  logsAPI.logs.setGlobalLoggerProvider(provider);
}

export async function stopLocalLogging() {
  await provider?.shutdown();
  provider = null;
}

export const getLogger = () => logsAPI.logs.getLogger('app');
export const paths = { LOG_DIR, LOG_FILE };

OTel-Logs: LoggerProvider/Logger/Emit sind das vorgesehene Modell. 

2) Dev-Mode-Screen (Toggle, View, Share)

// screens/DevLogsScreen.tsx
import React, { useEffect, useState } from 'react';
import { View, Text, Switch, FlatList, Button } from 'react-native';
import RNFS from 'react-native-fs';
import Share from 'react-native-share';
import { startLocalLogging, stopLocalLogging, getLogger, paths } from '../logging/otel';

export default function DevLogsScreen() {
  const [enabled, setEnabled] = useState(false);
  const [rows, setRows] = useState<any[]>([]);

  useEffect(() => {
    let id: any;
    if (enabled) {
      startLocalLogging().then(() => getLogger().emit({ severityText: 'INFO', body: 'Local logging enabled' }));
      id = setInterval(async () => {
        try {
          const txt = await RNFS.readFile(paths.LOG_FILE, 'utf8');
          const lines = txt.trim().split('\n').slice(-400).map(l => JSON.parse(l));
          setRows(lines.reverse());
        } catch {}
      }, 1500);
    } else {
      stopLocalLogging();
      setRows([]);
    }
    return () => { if (id) clearInterval(id); };
  }, [enabled]);

  const share = async () => {
    await Share.open({ url: 'file://' + paths.LOG_FILE, type: 'application/json', failOnCancel: false });
  };

  return (
    <View style={{ flex:1, padding:16, gap:12 }}>
      <View style={{ flexDirection:'row', justifyContent:'space-between', alignItems:'center' }}>
        <Text>Lokales Logging</Text>
        <Switch value={enabled} onValueChange={setEnabled} />
      </View>

      <Button title="Teilen (JSONL)" onPress={share} />
      <FlatList
        data={rows}
        keyExtractor={(_,i) => String(i)}
        renderItem={({item}) => <Text>{item.level}  {item.body}</Text>}
      />
    </View>
  );
}

Teilen via RN-Share/react-native-share. 

3) Rotation & Retention (Skizze)

•	Neue Datei pro Tag (app-logs-YYYY-MM-DD.jsonl) oder nach z. B. 2 MB.
•	Beim App-Start: alte Dateien > 14 Tage löschen (konfigurierbar).

Sicherheits- & Datenschutz-Leitplanken

•	Default: aus; explizites Opt-In im Dev-Mode.
•	Whitelist-Logging: Nur freigegebene Schlüssel/Attribute; keine PII, keine Tokens/Secrets; redigiere Muster (z. B. E-Mails, IBANs) vor dem Schreiben. (OWASP: „avoid logging sensitive data“)  
•	Speicherort: App-Sandbox, iOS ohne iCloud-Backup (Backup-Exklusion).  
•	Integrität/Schutz: Nur App-Zugriff; optional Verschlüsselung der Logdatei (Follow-Up).
•	Löschbarkeit: „Alles löschen“-Button in der UI.
•	Transparenz: Kurzer Hinweistext im Dev-Mode-Screen.

Akzeptanzkriterien (DoD)

1.	Logging ist standardmäßig deaktiviert; Status persistiert app-lokal (nur für Dev-Mode).
2.	Bei aktiviertem Logging werden Events als JSON-Lines in Datei geschrieben; Datei existiert nur im aktivierten Zustand.  
3.	Dev-Mode-Screen zeigt die letzten N Einträge (mind. 400) ohne spürbare UI-Lags.
4.	Share-Sheet kann aktuelle Logdatei erfolgreich teilen.  
5.	Rotation & Retention: max. Dateigröße/Tag; automatische Bereinigung nach konfigurierbarer Frist.
6.	Keine PII/Secrets in Logs (statische Checks + Tests, OWASP-konform).  
7.	iOS-Logs sind von iCloud-Backups ausgeschlossen.  

Testplan

•	Unit:
•	FileLogExporter schreibt valide JSON-Lines (pro Zeile valides JSON).
•	Redaktions-/Whitelist-Funktionen (PII-Muster).
•	Integration:
•	Toggle → Start/Stop-Pipeline; Append-Pfad; Rotation.
•	Share-Flow (Mock/CI mit File-URL).
•	E2E (Device/Simulator):
•	UI-Responsiveness bei 1k/5k Zeilen.
•	iOS: Ordner mit NSURLIsExcludedFromBackupKey angelegt.  
•	Static/Policy:
•	Linter-Regel verbietet logger-Aufrufe mit „verbotenen Keys“ (PWD, token, …).
•	CI-Job scannt Log-Fixtures auf PII-Muster.

Risiken & Gegenmaßnahmen

•	RNFS-Maintenance: Upstream schwankte teils; ggf. gepflegten Fork evaluieren.  
•	Performance: Batch-Größe/Flush-Interval feinjustieren; UI zeigt nur letzte N Einträge.
•	Fehlkonfiguration (PII): Whitelist-Ansatz + Tests + Code Reviews (OWASP).  

Alternativen

•	SQLite-Storage: bessere Queries, aber höherer Aufwand.
•	Ohne OTel: Custom-Logger; schneller, aber weniger standardkonform/korrigierbar.
•	Collector on-device: Overkill für unseren Use-Case.

Aufgaben (Checklist)

•	logging/ Modul (Port LogService, Adapter OtelLogService)
•	FileLogExporter + Rotation + Retention
•	Dev-Mode-Screen (Toggle, List, Share, Clear)
•	PII-Whitelist + Redaktions-Helper + Tests (Unit/Integration)
•	iOS: NSURLIsExcludedFromBackupKey beim Log-Ordner setzen  
•	Android/iOS-Permissions/Manifeste prüfen (keine unnötigen Berechtigungen)
•	CI: JSONL-Lint + PII-Pattern-Scan
•	Docs: Kurze Developer-Readme „Wie loggen“ + Troubleshooting

Referenzen (Auswahl)

•	OpenTelemetry Logs API/Spezifikationen.  
•	OTLP Spezifikation & HTTP/JSON-Konfiguration.  
•	JSON-Lines (NDJSON) Format.  
•	React Native Share / react-native-share.  
•	react-native-fs & iOS Backup-Exklusion.  
•	DSGVO Datenminimierung.  
•	OWASP (keine sensitiven Daten in Logs, Mobile-Leitlinien).  

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Status

⚙️ In Progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions