Protocol 15: one shared session log on the server, chat never logged, Mark a problem
Every app streams its redacted diagnostic lines (server time, batched every 10 s, gzip, buffered while offline) to POST /l/; the server merges them with its own into session-YYYY-MM-DD.log in time order, tagged by who. Opt-out in Settings > About; Mark a problem (Ctrl+Shift+B, the player menus, About) writes a marker with an optional note. Copy diagnostics can include the server's session log. Chat text never reaches any log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4ab37c55cf
commit
7487b4deb1
29 files changed
+1380
-47
No files matched your search
@@ -0,0 +1,87 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:sync_relay_core/core.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('a batch survives gzip both ways; a bomb or junk is refused', () {
|
||||
const b = DiagBatch(
|
||||
version: 'WT 1.0',
|
||||
lines: [DiagLine(t: 5, level: 'WARNING', logger: 'mpv', message: 'x\n error: y')],
|
||||
dropped: 2,
|
||||
marks: [DiagMark(t: 7, note: 'lag')]);
|
||||
final back = DiagBatch.decode(b.encode());
|
||||
expect(back.toJson(), b.toJson());
|
||||
expect(DiagBatch.decode(const DiagBatch(share: false).encode()).share, isFalse);
|
||||
expect(() => DiagBatch.decode(gzip.encode(List.filled(2 << 20, 32)), maxBytes: 1 << 20), throwsFormatException);
|
||||
expect(() => DiagBatch.decode(utf8.encode('plain')), throwsFormatException);
|
||||
});
|
||||
|
||||
group('DiagUploader', () {
|
||||
late HttpServer http;
|
||||
final got = <DiagBatch>[];
|
||||
var accept = true;
|
||||
setUp(() async {
|
||||
got.clear();
|
||||
accept = true;
|
||||
http = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||
http.listen((req) async {
|
||||
final body = await req.fold<List<int>>([], (a, c) => a..addAll(c));
|
||||
if (accept) got.add(DiagBatch.decode(body));
|
||||
req.response.statusCode = accept ? 204 : 503;
|
||||
await req.response.close();
|
||||
});
|
||||
});
|
||||
tearDown(() => http.close(force: true));
|
||||
|
||||
DiagUploader make({bool connected = true, int buffer = 1 << 20}) => DiagUploader(
|
||||
target: () => connected ? (base: Uri.parse('http://127.0.0.1:${http.port}'), token: 't', offsetMs: 1000) : null,
|
||||
version: 'WT 1.0',
|
||||
bufferBytes: buffer);
|
||||
|
||||
test('sends its records on the server clock, not the hosted server\'s; keeps them while it cannot', () async {
|
||||
final u = make()..start();
|
||||
Logger('relay').info('hello api_key=abc');
|
||||
Logger('server.relay').info('the server logs this itself');
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
accept = false;
|
||||
expect(await u.flush(), isFalse);
|
||||
expect(u.pending, 1);
|
||||
accept = true;
|
||||
expect(await u.flush(), isTrue);
|
||||
final line = got.single.lines.single;
|
||||
expect(line.message, 'hello api_key=***');
|
||||
expect(line.logger, 'relay');
|
||||
expect(got.single.version, 'WT 1.0');
|
||||
await u.stop();
|
||||
});
|
||||
|
||||
test('a full buffer drops the oldest and says how many', () async {
|
||||
final u = make(buffer: 400);
|
||||
for (var i = 0; i < 10; i++) {
|
||||
u.add(DiagLine(t: i, level: 'INFO', logger: 'a', message: 'line $i ${'x' * 40}'));
|
||||
}
|
||||
expect(await u.flush(), isTrue);
|
||||
expect(got.single.dropped, greaterThan(0));
|
||||
expect(got.single.lines.last.message, startsWith('line 9'));
|
||||
expect(got.single.lines.first.t, greaterThanOrEqualTo(1000), reason: 'offset to the server clock');
|
||||
});
|
||||
|
||||
test('opted out: no lines, one "does not share" per connection; marks still go', () async {
|
||||
final u = make()
|
||||
..share = false
|
||||
..start();
|
||||
Logger('relay').info('private');
|
||||
expect(await u.flush(), isTrue);
|
||||
expect(await u.flush(), isTrue);
|
||||
expect(got.length, 1);
|
||||
expect(got.single.share, isFalse);
|
||||
expect(got.single.lines, isEmpty);
|
||||
expect(await u.markProblem('it\nfroze'), isTrue);
|
||||
expect(got.last.marks.single.note, 'it froze');
|
||||
await u.stop();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -19,6 +19,7 @@ void main() {
|
||||
expect(redactLog('open http://1.2.3.4:8080/r/default/$tok/2/0/name'), 'open http://1.2.3.4:8080/r/default/***/2/0/name');
|
||||
expect(redactLog('ws://h:1/r/default/$tok/source?q=7'), 'ws://h:1/r/default/***/source?q=…');
|
||||
expect(redactLog('/m/default/$tok/file.mkv range=bytes=0-'), '/m/default/***/file.mkv range=bytes=0-');
|
||||
expect(redactLog('POST /l/default/$tok'), 'POST /l/default/***');
|
||||
expect(redactLog('edl://!no_clip;%160%http://h/r/default/$tok/2/0/n;!new_stream;!no_clip;%160%http://h/r/default/$tok/2/1/n'),
|
||||
isNot(contains(tok)));
|
||||
});
|
||||
@@ -85,5 +86,16 @@ void main() {
|
||||
expect(small.files().map((x) => x.uri.pathSegments.last), ['app-2026-10-12.1.log', 'app-2026-10-13.log']);
|
||||
small.close();
|
||||
});
|
||||
|
||||
test('the session log shares the total budget, never touching another writer\'s today', () {
|
||||
final now = DateTime(2026, 10, 12, 10);
|
||||
File('${dir.path}/session-2026-10-10.log').writeAsStringSync('s' * 100);
|
||||
File('${dir.path}/session-2026-10-12.log').writeAsStringSync('s' * 100);
|
||||
File('${dir.path}/app-2026-10-11.log').writeAsStringSync('a' * 100);
|
||||
final f = LogFiles(dir.path, maxTotalBytes: 150, shareWith: const ['session'], now: () => now)..open();
|
||||
final left = dir.listSync().map((e) => e.uri.pathSegments.last).toSet();
|
||||
expect(left, {'session-2026-10-12.log', 'app-2026-10-12.log'});
|
||||
f.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user