blob: 5466f59848f95a52845729552321a3bb0f995ef9 [file] [log] [blame]
// Copyright 2017 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:convert';
import 'dart:developer' show Timeline;
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'dart:typed_data';
import 'package:fidl_fuchsia_logger/fidl.dart' show LogSinkProxy;
import 'package:fidl_fuchsia_logger/fidl_async.dart' as logger_async
show LogSinkProxy;
import 'package:lib.app.dart/app.dart' show connectToService, StartupContext;
import 'package:lib.app.dart/app_async.dart' as app_async
show connectToService, StartupContext;
import 'package:logging/logging.dart' show Level;
import 'package:stack_trace/stack_trace.dart';
import 'package:zircon/zircon.dart' as zircon;
import 'fuchsia_log_record.dart';
import 'fuchsia_logger.dart';
const int _maxGlobalTags = 3;
const int _maxCombinedTags = 5;
const int _maxTagLength = 63;
const int _socketBufferLength = 2032;
final Map<Level, int> _enumToFuchsiaLevelMap = <Level, int>{
Level.FINEST: -4,
Level.FINER: -3,
Level.FINE: -2,
Level.CONFIG: -1,
Level.INFO: 0,
Level.WARNING: 1,
Level.SEVERE: 2,
Level.SHOUT: 3,
};
const int _unexpectedLoggingLevel = 100;
/// Method to write a single log message to a single output medium.
/// Solutions are provided to write to stdout and a fuchsia Socket.
typedef LogWriter = void Function(LogWriterMessage message);
/// Interim method that sets up the logger using the new-style bindings for service resolution.
void setupLoggerAsync({
String name,
Level level,
bool forceShowCodeLocation,
bool logToStdoutForTest,
List<String> globalTags,
zircon.Socket logSocket,
LogWriter logWriter,
}) {
FutureOr<zircon.Socket> sock;
if (logSocket == null) {
final socketPair = zircon.SocketPair(zircon.Socket.DATAGRAM);
final logSinkProxy = logger_async.LogSinkProxy();
final completer = Completer<zircon.Socket>();
app_async
.connectToService(
app_async.StartupContext.fromStartupInfo().environmentServices,
logSinkProxy.ctrl,
)
.then((_) => logSinkProxy.connect(socketPair.second))
.then((_) => completer.complete(socketPair.first))
.catchError(completer.completeError);
sock = completer.future;
} else {
sock = logSocket;
}
setupLogger(
name: name,
level: level,
forceShowCodeLocation: forceShowCodeLocation,
logToStdoutForTest: logToStdoutForTest,
globalTags: globalTags,
logSocket: sock,
logWriter: logWriter);
}
/// Sets up the default logger for the current Dart application.
///
/// Every Dart application should call this [setupLogger] function in their main
/// before calling the actual log statements.
///
/// The provided [name] will be used for displaying the scope, and this name
/// will default to the last segment (i.e. basename) of the application url.
///
/// If [level] is provided, only the log messages of which level is greater than
/// equal to the provided [level] will be shown. If not provided, it defaults to
/// [Level.INFO].
///
/// By default, the caller code location is automatically added in checked mode
/// and not in production mode, because it is relatively expensive to calculate
/// the code location. If [forceShowCodeLocation] is set to true, the location
/// will be added in production mode as well.
///
/// If [globalTags] is provided, these tags will be added to each message logged
/// via this logger.
///
/// [logToStdoutForTest] will redirect log output to stdout. This should only be
/// used when debugging tests that run on device to mix the log output with
/// the messages from the test infrastructure. It has no effect for on-host
/// tests and should not be included in production code.
///
/// [logWriter] and [logSocket] are intended only for testing purposes.
void setupLogger({
String name,
Level level,
bool forceShowCodeLocation,
bool logToStdoutForTest,
List<String> globalTags,
FutureOr<zircon.Socket> logSocket,
LogWriter logWriter,
}) {
final String scopeName = name ??
Platform.script?.pathSegments?.lastWhere((_) => true, orElse: () => null);
final List<String> approvedTags = _verifyGlobalTags(globalTags);
log = FuchsiaLogger(level ?? Level.ALL);
// This code decides how to log messages. Here's the why:
// If not running on zircon, there is no sys_logger so must use stdout.
// The author can force stdout via flag to assist is debugging tests.
// logSocket and logWriter are used for internal logging tests.
LogWriter activeLogWriter;
if (logWriter != null) {
activeLogWriter = logWriter;
} else if (logToStdoutForTest != true &&
(Platform.isFuchsia || logSocket != null)) {
_logSocket = logSocket ?? _connectToLoggerSocket();
activeLogWriter = writeLogToSocket;
} else {
activeLogWriter = writeLogToStdout;
}
_loggerName = scopeName;
bool inCheckedMode = false;
assert(() {
inCheckedMode = true;
return true;
}());
log.onRecord.listen((FuchsiaLogRecord rec) {
String codeLocation;
if (forceShowCodeLocation ?? inCheckedMode) {
final Trace trace = Trace.current();
final Frame callerFrame = _findCallerFrame(trace);
if (callerFrame != null) {
if (callerFrame.uri.pathSegments.isNotEmpty) {
final String filename = callerFrame.uri.pathSegments.last;
final String line =
callerFrame.line != null ? '(${callerFrame.line})' : '';
codeLocation = '$filename$line';
}
}
}
activeLogWriter(LogWriterMessage(
logRecord: rec,
scopeName: scopeName,
codeLocation: codeLocation,
tags: approvedTags,
));
});
}
/// Create a Socket and connect it to the FIDL logger
zircon.Socket _connectToLoggerSocket() {
final socketPair = zircon.SocketPair(zircon.Socket.DATAGRAM);
final logSinkProxy = LogSinkProxy();
connectToService(
StartupContext.fromStartupInfo().environmentServices,
logSinkProxy.ctrl,
);
logSinkProxy.connect(socketPair.second);
return socketPair.first;
}
/// All information required to construct a log message to either stdout or
/// Zircon logging Socket.
class LogWriterMessage {
/// Log record created by fuchsiaLogger
final FuchsiaLogRecord logRecord;
/// Name from the logger, typically the module name
final String scopeName;
/// If specified, file name and line number where this log message originated
final String codeLocation;
/// Identifying tags to associate with this logging message
final List<String> tags;
/// Constructor
LogWriterMessage(
{this.logRecord, this.scopeName, this.codeLocation, this.tags});
}
/// The default logger to be used by dart applications. Each application should
/// call [setupLogger()] in their main function to properly configure it.
FuchsiaLogger log = FuchsiaLogger(Level.ALL)
..onRecord.listen((FuchsiaLogRecord rec) {
print('WARNING: The logger is not initialized properly.');
print('WARNING: Please call setupLogger() from your main function.');
print('[${rec.level}] ${rec.message}');
});
/// The name of the logger.
String _loggerName = 'uninitialized';
/// Socket used to write to the universal Zircon logger.
FutureOr<zircon.Socket> _logSocket;
// Enforce limits on number of global tags and length of each tag
List<String> _verifyGlobalTags(List<String> globalTags) {
List<String> result = <String>[];
if (globalTags != null) {
if (globalTags.length > _maxGlobalTags) {
print('WARNING: Logger initialized with > $_maxGlobalTags tags.');
print('WARNING: Later tags will be ignored.');
}
for (int i = 0; i < _maxGlobalTags && i < globalTags.length; i++) {
String s = globalTags[i];
if (s.length > _maxTagLength) {
print('WARNING: Logger tags limited to $_maxTagLength characters.');
print('WARNING: Tag "$s" will be truncated.');
s = s.substring(0, _maxTagLength);
}
result.add(s);
}
}
return result;
}
/// LogWriter that outputs to stdout in a similar format the original fuchsia
/// logger.
void writeLogToStdout(LogWriterMessage message) {
final List<dynamic> scopes = <dynamic>[
_getLevelString(message.logRecord.level),
];
if (message.scopeName != null) {
scopes.add(message.scopeName);
}
if (message.codeLocation != null) {
scopes.add(message.codeLocation);
}
message.tags.forEach(scopes.add);
String scopesString = scopes.join(':');
if (message.logRecord.error != null) {
print(
'[$scopesString] ${message.logRecord.message}: ${message.logRecord.error}');
} else {
print('[$scopesString] ${message.logRecord.message}');
}
if (message.logRecord.stackTrace != null) {
print('${message.logRecord.stackTrace}');
}
}
int _convertLogLevel(Level logLevel) =>
_enumToFuchsiaLevelMap[logLevel] ?? _unexpectedLoggingLevel;
/// Format log message zircon socket connected to Log Viewer
void writeLogToSocket(LogWriterMessage message) {
ByteData bytes = ByteData(_socketBufferLength)
..setUint64(0, pid, Endian.little)
..setUint64(8, Isolate.current.hashCode, Endian.little)
..setUint64(16, message.logRecord.systemTime, Endian.little)
..setInt32(24, _convertLogLevel(message.logRecord.level), Endian.little)
..setUint32(28, 0, Endian.little); // TODO droppedLogs
int byteOffset = 32;
// Write global tags
int totalTagCount = 0;
if (message.scopeName != null) {
byteOffset = _setTag(bytes, byteOffset, message.scopeName);
totalTagCount++;
}
if (message.codeLocation != null) {
byteOffset = _setTag(bytes, byteOffset, message.codeLocation);
totalTagCount++;
}
for (String tag in message.tags) {
if (tag != null && totalTagCount < _maxCombinedTags) {
byteOffset = _setTag(bytes, byteOffset, tag);
totalTagCount++;
}
}
// Local tags are currently not supported by dart.
bytes.setUint8(byteOffset++, 0);
// Write message
byteOffset = _setString(bytes, byteOffset, message.logRecord.message,
_socketBufferLength - byteOffset - 1);
if (message.logRecord.error != null) {
byteOffset = _setString(
bytes, byteOffset, ': ', _socketBufferLength - byteOffset - 1);
byteOffset = _setString(
bytes,
byteOffset,
message.logRecord.error.toString(),
_socketBufferLength - byteOffset - 1);
}
if (message.logRecord.stackTrace != null) {
byteOffset = _setString(
bytes, byteOffset, '\n', _socketBufferLength - byteOffset - 1);
byteOffset = _setString(
bytes,
byteOffset,
message.logRecord.stackTrace.toString(),
_socketBufferLength - byteOffset - 1);
}
bytes.setUint8(byteOffset++, 0);
assert(byteOffset <= _socketBufferLength);
void writeBytes(zircon.Socket sock) {
sock.write(ByteData.view(bytes.buffer, 0, byteOffset));
}
if (_logSocket is Future) {
// need to wrap in a function to avoid casting errors
_writeToSocketFuture(_logSocket, writeBytes);
} else {
writeBytes(_logSocket);
}
}
void _writeToSocketFuture(Future<zircon.Socket> socketFuture,
void Function(zircon.Socket) writer) =>
socketFuture.then(writer);
// Write a string to ByteData with a leading length byte. Return the byteOffstet
// to use for the next value.
int _setTag(ByteData bytes, int byteOffset, String tag) {
if (tag == null || tag == 'null') {
return byteOffset;
}
int nextOffset = _setString(bytes, byteOffset + 1, tag, _maxTagLength);
bytes.setUint8(byteOffset, nextOffset - byteOffset - 1);
return nextOffset;
}
// Wrie a non-terminated string to ByteData. Return the byteOffset to use for
// the terminating byte or the next value.
int _setString(ByteData bytes, int firstByteOffset, String value, int maxLen) {
if (value == null || value.isEmpty) {
return firstByteOffset;
}
List<int> charBytes = utf8.encode(value);
int len = min(charBytes.length, maxLen);
int byteOffset = firstByteOffset;
for (int i = 0; i < len; i++) {
bytes.setUint8(byteOffset++, charBytes[i]);
}
// If the string was truncated (and there is space), add an elipsis character.
if (len < charBytes.length && len >= 3) {
const int period = 46; // UTF8 value for '.'
for (int i = 1; i <= 3; i++) {
bytes.setUint8(byteOffset - i, period);
}
}
return byteOffset;
}
/// A convenient function for displaying the stack trace of the caller in the
/// console.
void showStackTrace() {
print(Trace.current(1).toString());
}
/// Emits an instant trace with [name] prefixed with [log]'s name
void trace(String name) {
Timeline.instantSync('$_loggerName $name');
}
/// From the given [Trace], finds the last [Frame] from this package
/// (lib.app.dart) and then the next [Frame] not in this package. That frame
/// represents whomever called the log function and is returned.
/// If no such [Frame] is found, returns `null`.
///
/// SEE: https://github.com/dart-lang/logging/issues/32
Frame _findCallerFrame(Trace trace) {
bool foundLogging = false;
Frame matchedFrame;
for (int i = 0; i < trace.frames.length; ++i) {
final Frame frame = trace.frames[i];
final bool loggingPackage = frame.package == 'lib.app.dart';
if (foundLogging && !loggingPackage) {
matchedFrame = frame;
}
foundLogging = loggingPackage;
}
return matchedFrame;
}
/// 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)';
}
if (level == Level.FINER) {
return 'VLOG(2)';
}
if (level == Level.FINEST) {
return 'VLOG(3)';
}
if (level == Level.SEVERE) {
return 'ERROR';
}
if (level == Level.SHOUT) {
return 'FATAL';
}
return level.toString();
}