blob: 551259987eaaad46c77dcc4074f53f5876cf24dd [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import '_log_message.dart';
const int _maxGlobalTags = 4; // leave one slot for code location
const int _maxTagLength = 63;
final _fileNameRegex = RegExp(r'(\w+\.dart)');
final _lineNumberRegex = RegExp(r'\.dart:(\d+)');
/// The base class for which log writers will inherit from. This class is
/// used to pipe logs from the onRecord stream
abstract class LogWriter {
List<String> _globalTags = const [];
StreamController<LogMessage> _controller;
/// If set to true, this method will include the stack trace
/// in each log record so we can later extract out the call site.
/// This is a heavy operation and should be used with caution.
bool forceShowCodeLocation = false;
/// Constructor
LogWriter({
@required Logger logger,
bool shouldBufferLogs = false,
}) : assert(logger != null) {
void Function(LogMessage) onMessageFunc;
if (shouldBufferLogs) {
// create single subscription stream controller so that we buffer calls to the
// stream while we connect to the logger. This avoids dropping logs that
// come in while we wait.
_controller = StreamController<LogMessage>();
onMessageFunc = _controller.add;
} else {
onMessageFunc = onMessage;
}
logger.onRecord.listen(
(record) => onMessageFunc(_messageFromRecord(record)),
onDone: () => _controller?.close());
}
/// The global tags to add to each log record.
set globalTags(List<String> tags) => _globalTags = _verifyGlobalTags(tags);
/// Remaps the level string to the ones used in FTL.
String getLevelString(Level level) {
if (level == null) {
return null;
}
if (level == Level.FINE) {
return 'VLOG(1)';
} else if (level == Level.FINER) {
return 'VLOG(2)';
} else if (level == Level.FINEST) {
return 'VLOG(3)';
} else if (level == Level.SEVERE) {
return 'ERROR';
} else if (level == Level.SHOUT) {
return 'FATAL';
} else {
return level.toString();
}
}
LogMessage _messageFromRecord(LogRecord record) => LogMessage(
record: record,
processId: pid,
threadId: Isolate.current.hashCode,
tags: _tagsForLogMessage(),
);
/// A method for subclasses to implement to handle messages as they are
/// written
void onMessage(LogMessage message);
/// A method which is exposed to subclasses which can be used to indicate that
/// they are ready to start receiving messages.
@protected
void startListening(void Function(LogMessage) onMessage) =>
_controller.stream.listen(onMessage);
List<String> _verifyGlobalTags(List<String> tags) {
List<String> result = <String>[];
// make our own copy to allow us to remove null values an not change the
// original values
final incomingTags = List.of(tags)
..removeWhere((t) => t == null || t.isEmpty);
if (incomingTags != null) {
if (incomingTags.length > _maxGlobalTags) {
Logger.root.warning('Logger initialized with > $_maxGlobalTags tags.');
Logger.root.warning('Later tags will be ignored.');
}
for (int i = 0; i < _maxGlobalTags && i < incomingTags.length; i++) {
String s = incomingTags[i];
if (s.length > _maxTagLength) {
Logger.root
.warning('Logger tags limited to $_maxTagLength characters.');
Logger.root.warning('Tag "$s" will be truncated.');
s = s.substring(0, _maxTagLength);
}
result.add(s);
}
}
return result;
}
List<String> _tagsForLogMessage() {
if (forceShowCodeLocation) {
final codeLocation = _codeLocationFromStackTrace(StackTrace.current);
if (codeLocation != null && codeLocation.isNotEmpty) {
return List.of(_globalTags)..add(codeLocation);
}
}
return _globalTags;
}
String _codeLocationFromStackTrace(StackTrace stackTrace) {
final lines = stackTrace.toString().split('\n');
// There is no well supported way for getting the calling code location from
// the log line. In lieu of this, we use the following algorithm which is
// fragile and depends on the order which the logger methods are called. The
// algorithm is that we run through each line and look for the call to the
// line that contains 'Logger.log (package:logging/logging.dart' which is
// the call that comes after the user calls Logger.info, Logger.warn, etc.
// When this line is found we look 2 lines past for their call site.
const loggerLogLine = r'Logger.log (package:logging/logging.dart';
const logLineOffset = 2;
String codeLocation;
for (int lineNumber = 0; lineNumber < lines.length; lineNumber++) {
final line = lines[lineNumber];
if (line.contains(loggerLogLine)) {
if (lineNumber + logLineOffset < lines.length) {
codeLocation = lines[lineNumber + logLineOffset];
}
break;
}
}
return _extractCodeLocationFromLine(codeLocation);
}
String _extractCodeLocationFromLine(String line) {
if (line == null) {
return null;
}
String regexValue(RegExp regexp) {
final match = regexp.firstMatch(line);
return match != null ? match.group(1) : null;
}
final fileName = regexValue(_fileNameRegex);
if (fileName == null) {
return null;
}
final lineNumber = regexValue(_lineNumberRegex);
// Some environments don't give us the line number so we avoid failing
// completely if we have just the file name.
return lineNumber == null ? fileName : '$fileName($lineNumber)';
}
}