| // 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:convert' show utf8; |
| import 'dart:io'; |
| import 'dart:math'; |
| import 'dart:typed_data'; |
| |
| import 'package:logging/logging.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:zircon/zircon.dart'; |
| |
| const int _maxCombinedTags = 5; |
| const int _maxTagLength = 63; |
| const int _socketBufferLength = 2032; |
| const int _unexpectedLoggingLevel = 100; |
| |
| const int _zxClockMonotonic = 0; |
| 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, |
| }; |
| |
| /// A wrapper around [LogRecord] which appends additional data. This |
| /// is what is sent to the log writer when a record is received. |
| class LogMessage { |
| /// The initial log record |
| final LogRecord record; |
| |
| /// Any additional tags to append to the record. |
| final List<String> tags; |
| |
| /// The id of the process which this log message is associated with |
| final int processId; |
| |
| /// The id of the thread which this log message is associated with |
| final int threadId; |
| |
| /// The time that this message was created |
| final int systemTime = Platform.isFuchsia |
| ? System.clockGet(_zxClockMonotonic) |
| : new DateTime.now().microsecondsSinceEpoch * 1000; |
| |
| /// The default constructor |
| LogMessage({ |
| @required this.record, |
| @required this.processId, |
| @required this.threadId, |
| this.tags = const [], |
| }) : assert(record != null), |
| assert(processId != null), |
| assert(threadId != null); |
| |
| /// Converts this to a ByteData which can be used to send the message to the |
| /// log socket. |
| ByteData toBytes() { |
| ByteData bytes = ByteData(_socketBufferLength) |
| ..setUint64(0, processId, Endian.little) |
| ..setUint64(8, threadId, Endian.little) |
| ..setUint64(16, systemTime, Endian.little) |
| ..setInt32(24, _convertLogLevel(record.level), Endian.little) |
| ..setUint32(28, 0, Endian.little); // TODO(120860552) droppedLogs |
| int byteOffset = 32; |
| |
| // Write global tags |
| int totalTagCount = 0; |
| for (final tag in tags) { |
| if (tag != null && totalTagCount < _maxCombinedTags) { |
| byteOffset = _setTag(bytes, byteOffset, tag); |
| totalTagCount++; |
| } |
| } |
| |
| // We need to skip the local tags section since we do not support them |
| bytes.setUint8(byteOffset++, 0); |
| |
| // Write message |
| byteOffset = _setString(bytes, byteOffset, record.message, |
| _socketBufferLength - byteOffset - 1); |
| if (record.error != null) { |
| byteOffset = _setString( |
| bytes, byteOffset, ': ', _socketBufferLength - byteOffset - 1); |
| byteOffset = _setString(bytes, byteOffset, record.error.toString(), |
| _socketBufferLength - byteOffset - 1); |
| } |
| if (record.stackTrace != null) { |
| byteOffset = _setString( |
| bytes, byteOffset, '\n', _socketBufferLength - byteOffset - 1); |
| byteOffset = _setString(bytes, byteOffset, record.stackTrace.toString(), |
| _socketBufferLength - byteOffset - 1); |
| } |
| bytes.setUint8(byteOffset++, 0); |
| return bytes.buffer.asByteData(0, byteOffset); |
| } |
| |
| int _convertLogLevel(Level logLevel) => |
| _enumToFuchsiaLevelMap[logLevel] ?? _unexpectedLoggingLevel; |
| |
| /// Write a string to ByteData with a leading length byte. Return the |
| /// byteOffstet to use for the next value. 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; |
| } |
| |
| 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; |
| } |
| } |