blob: f9d2e79e750ae312d745d7b51ce721ff676e3594 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.commands.incident.sections;
import android.util.Log;
import android.util.PersistedLogProto;
import android.util.TextLogEntry;
import android.util.proto.ProtoOutputStream;
import com.android.commands.incident.ExecutionException;
import com.android.commands.incident.IncidentHelper;
import com.android.commands.incident.Section;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
/** PersistLogSection reads persisted logs and parses them into a PersistedLogProto. */
public class PersistLogSection implements Section {
private static final String TAG = "IH_PersistLog";
private static final String LOG_DIR = "/data/misc/logd/";
// Persist log files are named logcat, logcat.001, logcat.002, logcat.003, ...
private static final Pattern LOG_FILE_RE = Pattern.compile("logcat(\\.\\d+)?");
private static final Pattern BUFFER_BEGIN_RE =
Pattern.compile("--------- (?:beginning of|switch to) (.*)");
private static final Map<String, Long> SECTION_NAME_TO_ID = new HashMap<>();
private static final Map<Character, Integer> LOG_PRIORITY_MAP = new HashMap<>();
private static final String DEFAULT_BUFFER = "main";
static {
SECTION_NAME_TO_ID.put("main", PersistedLogProto.MAIN_LOGS);
SECTION_NAME_TO_ID.put("radio", PersistedLogProto.RADIO_LOGS);
SECTION_NAME_TO_ID.put("events", PersistedLogProto.EVENTS_LOGS);
SECTION_NAME_TO_ID.put("system", PersistedLogProto.SYSTEM_LOGS);
SECTION_NAME_TO_ID.put("crash", PersistedLogProto.CRASH_LOGS);
SECTION_NAME_TO_ID.put("kernel", PersistedLogProto.KERNEL_LOGS);
}
static {
LOG_PRIORITY_MAP.put('V', TextLogEntry.LOG_VERBOSE);
LOG_PRIORITY_MAP.put('D', TextLogEntry.LOG_DEBUG);
LOG_PRIORITY_MAP.put('I', TextLogEntry.LOG_INFO);
LOG_PRIORITY_MAP.put('W', TextLogEntry.LOG_WARN);
LOG_PRIORITY_MAP.put('E', TextLogEntry.LOG_ERROR);
LOG_PRIORITY_MAP.put('F', TextLogEntry.LOG_FATAL);
LOG_PRIORITY_MAP.put('S', TextLogEntry.LOG_SILENT);
}
/**
* Caches dates at 00:00:00 to epoch second elapsed conversion. There are only a few different
* dates in persisted logs in one device, and constructing DateTime object is relatively
* expensive.
*/
private Map<Integer, Long> mEpochTimeCache = new HashMap<>();
private ProtoOutputStream mProto;
private long mCurrFieldId;
private long mMaxBytes = Long.MAX_VALUE;
@Override
public void run(InputStream in, OutputStream out, List<String> args) throws ExecutionException {
parseArgs(args);
Path logDirPath = Paths.get(LOG_DIR);
if (!Files.exists(logDirPath)) {
IncidentHelper.log(Log.WARN, TAG, "Skip dump. " + logDirPath + " does not exist.");
return;
}
if (!Files.isReadable(logDirPath)) {
IncidentHelper.log(Log.WARN, TAG, "Skip dump. " + logDirPath + " is not readable.");
return;
}
mProto = new ProtoOutputStream(out);
setCurrentSection(DEFAULT_BUFFER);
final Matcher logFileRe = LOG_FILE_RE.matcher("");
// Need to process older log files first and write logs to proto in chronological order
// But we want to process only the latest ones if there is a size limit
try (Stream<File> stream = Files.list(logDirPath).map(Path::toFile)
.filter(f -> !f.isDirectory() && match(logFileRe, f.getName()) != null)
.sorted(Comparator.comparingLong(File::lastModified).reversed())) {
Iterator<File> iter = stream.iterator();
List<File> filesToProcess = new ArrayList<>();
long sumBytes = 0;
while (iter.hasNext()) {
File file = iter.next();
sumBytes += file.length();
if (sumBytes > mMaxBytes) {
break;
}
filesToProcess.add(file);
}
IncidentHelper.log(Log.INFO, TAG, "Limit # log files to " + filesToProcess.size());
filesToProcess.stream()
.sorted(Comparator.comparingLong(File::lastModified))
.forEachOrdered(this::processFile);
} catch (IOException e) {
throw new ExecutionException(e);
} finally {
mProto.flush();
}
IncidentHelper.log(Log.DEBUG, TAG, "Bytes written: " + mProto.getBytes().length);
}
private void parseArgs(List<String> args) {
Iterator<String> iter = args.iterator();
while (iter.hasNext()) {
String arg = iter.next();
if ("--limit".equals(arg) && iter.hasNext()) {
String sizeStr = iter.next().toLowerCase();
if (sizeStr.endsWith("mb")) {
mMaxBytes = Long.parseLong(sizeStr.replace("mb", "")) * 1024 * 1024;
} else if (sizeStr.endsWith("kb")) {
mMaxBytes = Long.parseLong(sizeStr.replace("kb", "")) * 1024;
} else {
mMaxBytes = Long.parseLong(sizeStr);
}
} else {
throw new IllegalArgumentException("Unknown argument: " + arg);
}
}
}
private void processFile(File file) {
final Matcher bufferBeginRe = BUFFER_BEGIN_RE.matcher("");
try (BufferedReader reader = Files.newBufferedReader(file.toPath(),
StandardCharsets.UTF_8)) {
String line;
Matcher m;
while ((line = reader.readLine()) != null) {
if ((m = match(bufferBeginRe, line)) != null) {
setCurrentSection(m.group(1));
continue;
}
parseLine(line);
}
} catch (IOException e) {
// Non-fatal error. We can skip and still process other files.
IncidentHelper.log(Log.WARN, TAG, "Error reading \"" + file + "\": " + e.getMessage());
}
IncidentHelper.log(Log.DEBUG, TAG, "Finished reading " + file);
}
private void setCurrentSection(String sectionName) {
Long sectionId = SECTION_NAME_TO_ID.get(sectionName);
if (sectionId == null) {
IncidentHelper.log(Log.WARN, TAG, "Section does not exist: " + sectionName);
sectionId = SECTION_NAME_TO_ID.get(DEFAULT_BUFFER);
}
mCurrFieldId = sectionId;
}
/**
* Parse a log line in the following format:
* 01-01 15:01:47.723501 2738 2895 I Exp_TAG: example log line
*
* It does not use RegExp for performance reasons. Using this RegExp "(\\d{2})-(\\d{2})\\s
* (\\d{2}):(\\d{2}):(\\d{2}).(\\d{6})\\s+(\\d+)\\s+(\\d+)\\s+(.)\\s+(.*?):\\s(.*)" is twice as
* slow as the current approach.
*/
private void parseLine(String line) {
long token = mProto.start(mCurrFieldId);
try {
mProto.write(TextLogEntry.SEC, getEpochSec(line));
// Nanosec is 15th to 20th digits of "10-01 02:57:27.710652" times 1000
mProto.write(TextLogEntry.NANOSEC, parseInt(line, 15, 21) * 1000L);
int start = nextNonBlank(line, 21);
int end = line.indexOf(' ', start + 1);
mProto.write(TextLogEntry.PID, parseInt(line, start, end));
start = nextNonBlank(line, end);
end = line.indexOf(' ', start + 1);
mProto.write(TextLogEntry.TID, parseInt(line, start, end));
start = nextNonBlank(line, end);
char priority = line.charAt(start);
mProto.write(TextLogEntry.PRIORITY,
LOG_PRIORITY_MAP.getOrDefault(priority, TextLogEntry.LOG_DEFAULT));
start = nextNonBlank(line, start + 1);
end = line.indexOf(": ", start);
mProto.write(TextLogEntry.TAG, line.substring(start, end).trim());
mProto.write(TextLogEntry.LOG, line.substring(Math.min(end + 2, line.length())));
} catch (RuntimeException e) {
// Error reporting is likely piped to /dev/null. Inserting it into the proto to make
// it more useful.
mProto.write(TextLogEntry.SEC, System.currentTimeMillis() / 1000);
mProto.write(TextLogEntry.PRIORITY, TextLogEntry.LOG_ERROR);
mProto.write(TextLogEntry.TAG, TAG);
mProto.write(TextLogEntry.LOG,
"Error parsing \"" + line + "\"" + ": " + e.getMessage());
}
mProto.end(token);
}
// ============== Below are util methods to parse log lines ==============
private static int nextNonBlank(String line, int start) {
for (int i = start; i < line.length(); i++) {
if (line.charAt(i) != ' ') {
return i;
}
}
return -1;
}
/**
* Gets the epoch second from the line string. Line starts with a fixed-length timestamp like
* "10-01 02:57:27.710652"
*/
private long getEpochSec(String line) {
int month = getDigit(line, 0) * 10 + getDigit(line, 1);
int day = getDigit(line, 3) * 10 + getDigit(line, 4);
int mmdd = month * 100 + day;
long epochSecBase = mEpochTimeCache.computeIfAbsent(mmdd, (key) -> {
final GregorianCalendar calendar = new GregorianCalendar();
calendar.set(Calendar.MONTH, (month + 12 - 1) % 12);
calendar.set(Calendar.DAY_OF_MONTH, day);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
// Date in log entries can never be in the future. If it happens, it means we are off
// by one year.
if (calendar.getTimeInMillis() > System.currentTimeMillis()) {
calendar.roll(Calendar.YEAR, /*amount=*/-1);
}
return calendar.getTimeInMillis() / 1000;
});
int hh = getDigit(line, 6) * 10 + getDigit(line, 7);
int mm = getDigit(line, 9) * 10 + getDigit(line, 10);
int ss = getDigit(line, 12) * 10 + getDigit(line, 13);
return epochSecBase + hh * 3600 + mm * 60 + ss;
}
private static int parseInt(String line, /*inclusive*/ int start, /*exclusive*/ int end) {
int num = 0;
for (int i = start; i < end; i++) {
num = num * 10 + getDigit(line, i);
}
return num;
}
private static int getDigit(String str, int pos) {
int digit = str.charAt(pos) - '0';
if (digit < 0 || digit > 9) {
throw new NumberFormatException("'" + str.charAt(pos) + "' is not a digit.");
}
return digit;
}
private static Matcher match(Matcher matcher, String text) {
matcher.reset(text);
return matcher.matches() ? matcher : null;
}
}