blob: ae3cf622a4f3afe7dd2634f50badb368cb19672a [file] [log] [blame]
/*
* Copyright (C) 2011 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.loganalysis.parser;
import com.android.loganalysis.item.LogcatItem;
import com.android.loganalysis.item.MiscLogcatItem;
import com.android.loganalysis.util.ArrayUtil;
import com.android.loganalysis.util.LogPatternUtil;
import com.android.loganalysis.util.LogTailUtil;
import java.io.BufferedReader;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* An {@link IParser} to handle logcat. The parser can handle the time and threadtime logcat
* formats.
* <p>
* Since the timestamps in the logcat do not have a year, the year can be set manually when the
* parser is created or through {@link #setYear(String)}. If a year is not set, the current year
* will be used.
* </p>
*/
public class LogcatParser implements IParser {
public static final String ANR = "ANR";
public static final String JAVA_CRASH = "JAVA_CRASH";
public static final String NATIVE_CRASH = "NATIVE_CRASH";
public static final String HIGH_CPU_USAGE = "HIGH_CPU_USAGE";
public static final String HIGH_MEMORY_USAGE = "HIGH_MEMORY_USAGE";
public static final String RUNTIME_RESTART = "RUNTIME_RESTART";
/**
* Match a single line of `logcat -v threadtime`, such as:
* 05-26 11:02:36.886 5689 5689 D AndroidRuntime: CheckJNI is OFF
*/
private static final Pattern THREADTIME_LINE = Pattern.compile(
"^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})\\s+" + /* timestamp [1] */
"(\\d+)\\s+(\\d+)\\s+([A-Z])\\s+" + /* pid/tid and log level [2-4] */
"(.+?)\\s*: (.*)$" /* tag and message [5-6]*/);
/**
* Match a single line of `logcat -v time`, such as:
* 06-04 02:32:14.002 D/dalvikvm( 236): GC_CONCURRENT freed 580K, 51% free [...]
*/
private static final Pattern TIME_LINE = Pattern.compile(
"^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})\\s+" + /* timestamp [1] */
"(\\w)/(.+?)\\(\\s*(\\d+)\\): (.*)$"); /* level, tag, pid, msg [2-5] */
/**
* Match: "*** FATAL EXCEPTION IN SYSTEM PROCESS: message"
*/
private static final Pattern SYSTEM_SERVER_CRASH = Pattern.compile(
"\\*\\*\\* FATAL EXCEPTION IN SYSTEM PROCESS:.*");
/**
* Match "Process: com.android.package, PID: 123" or "PID: 123"
*/
private static final Pattern JAVA_CRASH_PROCESS_PID = Pattern.compile(
"^(Process: (\\S+), )?PID: (\\d+)$");
/**
* Match a line thats printed when a non app java process starts.
*/
private static final Pattern JAVA_PROC_START = Pattern.compile("Calling main entry (.+)");
/**
* Class for storing logcat meta data for a particular grouped list of lines.
*/
private class LogcatData {
public Integer mPid = null;
public Integer mTid = null;
public Date mTime = null;
public String mLevel = null;
public String mTag = null;
public String mLastPreamble = null;
public String mProcPreamble = null;
public List<String> mLines = new LinkedList<String>();
public LogcatData(Integer pid, Integer tid, Date time, String level, String tag,
String lastPreamble, String procPreamble) {
mPid = pid;
mTid = tid;
mTime = time;
mLevel = level;
mTag = tag;
mLastPreamble = lastPreamble;
mProcPreamble = procPreamble;
}
}
private LogPatternUtil mPatternUtil = new LogPatternUtil();
private LogTailUtil mPreambleUtil = new LogTailUtil();
private String mYear = null;
LogcatItem mLogcat = null;
Map<String, LogcatData> mDataMap = new HashMap<String, LogcatData>();
List<LogcatData> mDataList = new LinkedList<LogcatData>();
private Date mStartTime = null;
private Date mStopTime = null;
private boolean mIsParsing = true;
private Map<Integer, String> mPids = new HashMap<Integer, String>();
private List<CrashTag> mJavaCrashTags = new ArrayList<>();
private List<CrashTag> mNativeCrashTags = new ArrayList<>();
/**
* Constructor for {@link LogcatParser}.
*/
public LogcatParser() {
// Add default tag for java crash
addJavaCrashTag("E", "AndroidRuntime", JAVA_CRASH);
addNativeCrashTag("I", "DEBUG");
addNativeCrashTag("F", "DEBUG");
initPatterns();
}
/**
* Constructor for {@link LogcatParser}.
*
* @param year The year as a string.
*/
public LogcatParser(String year) {
this();
setYear(year);
}
/**
* Sets the year for {@link LogcatParser}.
*
* @param year The year as a string.
*/
public void setYear(String year) {
mYear = year;
}
/**
* Parse a logcat from a {@link BufferedReader} into an {@link LogcatItem} object.
*
* @param input a {@link BufferedReader}.
* @return The {@link LogcatItem}.
* @see #parse(List)
*/
public LogcatItem parse(BufferedReader input) throws IOException {
String line;
while ((line = input.readLine()) != null) {
parseLine(line);
}
commit();
return mLogcat;
}
/**
* {@inheritDoc}
*
* @return The {@link LogcatItem}.
*/
@Override
public LogcatItem parse(List<String> lines) {
for (String line : lines) {
parseLine(line);
}
commit();
return mLogcat;
}
/**
* Clear the existing {@link LogcatItem}. The next parse will create a new one.
*/
public void clear() {
mLogcat = null;
mDataList.clear();
mDataMap.clear();
}
/**
* Parse a line of input.
*
* @param line The line to parse
*/
private void parseLine(String line) {
if ("".equals(line.trim())) {
return;
}
if (mLogcat == null) {
mLogcat = new LogcatItem();
}
Integer pid = null;
Integer tid = null;
Date time = null;
String level = null;
String tag = null;
String msg = null;
Matcher m = THREADTIME_LINE.matcher(line);
Matcher tm = TIME_LINE.matcher(line);
if (m.matches()) {
time = parseTime(m.group(1));
pid = Integer.parseInt(m.group(2));
tid = Integer.parseInt(m.group(3));
level = m.group(4);
tag = m.group(5);
msg = m.group(6);
} else if (tm.matches()) {
time = parseTime(tm.group(1));
level = tm.group(2);
tag = tm.group(3);
pid = Integer.parseInt(tm.group(4));
msg = tm.group(5);
}
if (time != null) {
if (mStartTime == null) {
mStartTime = time;
}
mStopTime = time;
}
// Don't parse any lines after device begins reboot until a new log is detected.
if ("I".equals(level) && "ShutdownThread".equals(tag) &&
Pattern.matches("Rebooting, reason: .*", msg)) {
mIsParsing = false;
}
if (Pattern.matches(".*--------- beginning of .*", line)) {
mIsParsing = true;
}
if (!mIsParsing || !(m.matches() || tm.matches())) {
return;
}
// When a non app java process starts add its pid to the map
Matcher pidMatcher = JAVA_PROC_START.matcher(msg);
if (pidMatcher.matches()) {
String name = pidMatcher.group(1);
mPids.put(pid, name);
}
// ANRs are separated either by different PID/TIDs or when AnrParser.START matches a line.
// The newest entry is kept in the dataMap for quick lookup while all entries are added to
// the list.
if ("E".equals(level) && "ActivityManager".equals(tag)) {
String key = encodeLine(pid, tid, level, tag);
LogcatData data;
if (!mDataMap.containsKey(key) || AnrParser.START.matcher(msg).matches()) {
data = new LogcatData(pid, tid, time, level, tag, mPreambleUtil.getLastTail(),
mPreambleUtil.getIdTail(pid));
mDataMap.put(key, data);
mDataList.add(data);
} else {
data = mDataMap.get(key);
}
data.mLines.add(msg);
}
// Native crashes are separated either by different PID/TIDs or when
// NativeCrashParser.FINGERPRINT matches a line. The newest entry is kept in the dataMap
// for quick lookup while all entries are added to the list.
if (anyNativeCrashTagMatches(level, tag)) {
String key = encodeLine(pid, tid, level, tag);
LogcatData data;
if (!mDataMap.containsKey(key) || NativeCrashParser.FINGERPRINT.matcher(msg).matches()) {
data = new LogcatData(pid, tid, time, level, tag, mPreambleUtil.getLastTail(),
mPreambleUtil.getIdTail(pid));
mDataMap.put(key, data);
mDataList.add(data);
} else {
data = mDataMap.get(key);
}
data.mLines.add(msg);
}
// PID and TID are enough to separate Java crashes.
if (anyJavaCrashTagMatches(level, tag)) {
String key = encodeLine(pid, tid, level, tag);
LogcatData data;
if (!mDataMap.containsKey(key)) {
data = new LogcatData(pid, tid, time, level, tag, mPreambleUtil.getLastTail(),
mPreambleUtil.getIdTail(pid));
mDataMap.put(key, data);
mDataList.add(data);
} else {
data = mDataMap.get(key);
}
data.mLines.add(msg);
}
// Check the message here but add it in commit()
if (mPatternUtil.checkMessage(msg, new ExtrasPattern(level, tag)) != null) {
LogcatData data = new LogcatData(pid, tid, time, level, tag,
mPreambleUtil.getLastTail(), mPreambleUtil.getIdTail(pid));
data.mLines.add(msg);
mDataList.add(data);
}
// After parsing the line, add it the the buffer for the preambles.
mPreambleUtil.addLine(pid, line);
}
/**
* Signal that the input has finished.
*/
private void commit() {
if (mLogcat == null) {
return;
}
for (LogcatData data : mDataList) {
MiscLogcatItem item = null;
if ("E".equals(data.mLevel) && "ActivityManager".equals(data.mTag)) {
item = new AnrParser().parse(data.mLines);
} else if (anyJavaCrashTagMatches(data.mLevel, data.mTag)) {
// Get the process name/PID from the Java crash, then pass the rest of the lines to
// the parser.
Integer pid = null;
String app = null;
for (int i = 0; i < data.mLines.size(); i++) {
String line = data.mLines.get(i);
Matcher m = JAVA_CRASH_PROCESS_PID.matcher(line);
if (m.matches()) {
app = m.group(2);
pid = Integer.valueOf(m.group(3));
data.mLines = data.mLines.subList(i + 1, data.mLines.size());
break;
}
m = SYSTEM_SERVER_CRASH.matcher(line);
if (m.matches()) {
app = mPids.get(data.mPid);
if (app == null) {
app = "system_server";
}
data.mLines = data.mLines.subList(i + 1, data.mLines.size());
break;
}
}
item = new JavaCrashParser().parse(data.mLines);
if (item != null) {
item.setApp(app);
item.setPid(pid);
item.setCategory(getJavaCrashCategory(data.mLevel, data.mTag));
}
} else if (anyNativeCrashTagMatches(data.mLevel, data.mTag)) {
// CLog.v("Parsing native crash: %s", data.mLines);
item = new NativeCrashParser().parse(data.mLines);
} else {
String msg = ArrayUtil.join("\n", data.mLines);
String category = mPatternUtil.checkMessage(msg, new ExtrasPattern(
data.mLevel, data.mTag));
if (category != null) {
MiscLogcatItem logcatItem = new MiscLogcatItem();
logcatItem.setCategory(category);
logcatItem.setStack(msg);
item = logcatItem;
}
}
if (item != null) {
item.setEventTime(data.mTime);
if (item.getPid() == null) {
item.setPid(data.mPid);
item.setTid(data.mTid);
}
item.setLastPreamble(data.mLastPreamble);
item.setProcessPreamble(data.mProcPreamble);
item.setTag(data.mTag);
mLogcat.addEvent(item);
}
}
mLogcat.setStartTime(mStartTime);
mLogcat.setStopTime(mStopTime);
}
/**
* Create an identifier that "should" be unique for a given logcat. In practice, we do use it as
* a unique identifier.
*/
private static String encodeLine(Integer pid, Integer tid, String level, String tag) {
if (tid == null) {
return String.format("%d|%s|%s", pid, level, tag);
}
return String.format("%d|%d|%s|%s", pid, tid, level, tag);
}
/**
* Parse the timestamp and return a {@link Date}. If year is not set, the current year will be
* used.
*
* @param timeStr The timestamp in the format {@code MM-dd HH:mm:ss.SSS}.
* @return The {@link Date}.
*/
private Date parseTime(String timeStr) {
// If year is null, just use the current year.
if (mYear == null) {
DateFormat yearFormatter = new SimpleDateFormat("yyyy");
mYear = yearFormatter.format(new Date());
}
DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
try {
return formatter.parse(String.format("%s-%s", mYear, timeStr));
} catch (ParseException e) {
// CLog.e("Could not parse time string %s", timeStr);
return null;
}
}
private void initPatterns() {
// High CPU usage
mPatternUtil.addPattern(Pattern.compile(".* timed out \\(is the CPU pegged\\?\\).*"),
HIGH_CPU_USAGE);
// High memory usage
mPatternUtil.addPattern(Pattern.compile(
"GetBufferLock timed out for thread \\d+ buffer .*"), HIGH_MEMORY_USAGE);
// Runtime restarts
mPatternUtil.addPattern(Pattern.compile("\\*\\*\\* WATCHDOG KILLING SYSTEM PROCESS.*"),
RUNTIME_RESTART);
}
/**
* Adds a custom, more complex pattern to LogcatParser for parsing out of logcat events.
* Any matched events are then assigned to the category name provided, and can be grabbed
* via LogcatParser's .getMiscEvents("yourCustomCategoryName") method.
* Considers null messages, levels, or tags to be wildcards.
*
* @param pattern The regex representing the pattern to match for, or null for wildcard.
* @param level The name of the level to match for, or null for wildcard.
* @param tag The name of the tag to match for, or null for wildcard.
* @param category Assign any matching logcat events to this category name, for later retrieval
*/
public void addPattern(Pattern pattern, String level, String tag, String category) {
/* count null message as wildcard */
if (pattern == null) {
pattern = Pattern.compile(".*");
}
mPatternUtil.addPattern(pattern, new ExtrasPattern(level, tag), category);
}
/**
* Used internally for bundling up extra pattern criteria for more advanced pattern matching.
*/
private class ExtrasPattern {
public String mLevel;
public String mTag;
public ExtrasPattern(String level, String tag) {
mLevel = level;
mTag = tag;
}
/**
* Override Object.equals to match based on the level & tag patterns,
* while also counting null level & tag patterns as wildcards.
*
* @param otherObj the object we're matching the level & tag patterns to.
* @return true if otherObj's extras match, false otherwise
*/
@Override
public boolean equals(Object otherObj) {
if (otherObj instanceof ExtrasPattern) {
// Treat objects as equal only if the obj's level and tag match.
// Treat null as a wildcard.
ExtrasPattern other = (ExtrasPattern) otherObj;
if ((mLevel == null || other.mLevel == null || mLevel.equals(other.mLevel)) &&
(mTag == null || other.mTag == null || mTag.equals(other.mTag))) {
return true;
}
}
return false;
}
}
/**
* Allows Java crashes to be parsed from multiple log levels and tags. Normally the crashes
* are error level messages from AndroidRuntime, but they could also be from other sources.
* Use this method to parse java crashes from those other sources.
*
* @param level log level on which to look for java crashes
* @param tag log tag where to look for java crashes
*/
public void addJavaCrashTag(String level, String tag, String category) {
mJavaCrashTags.add(new CrashTag(level, tag, category));
}
/**
* Allows native crashes to be parsed from multiple log levels and tags. The default levels
* are "I DEBUG" and "F DEBUG".
*
* @param level log level on which to look for native crashes
* @param tag log tag where to look for native crashes
*/
private void addNativeCrashTag(String level, String tag) {
mNativeCrashTags.add(new CrashTag(level, tag, NATIVE_CRASH));
}
/**
* Determines if any of the Java crash tags is matching a logcat line.
*
* @param level log level of the logcat line
* @param tag tag of the logcat line
* @return True if any Java crash tag matches the current level and tag. False otherwise.
*/
private boolean anyJavaCrashTagMatches(String level, String tag) {
return findCrashTag(mJavaCrashTags, level, tag) != null;
}
/**
* Determines if any of the native crash tags is matching a logcat line.
*
* @param level log level of the logcat line
* @param tag tag of the logcat line
* @return True if any native crash tag matches the current level and tag. False otherwise.
*/
private boolean anyNativeCrashTagMatches(String level, String tag) {
return findCrashTag(mNativeCrashTags, level, tag) != null;
}
/**
* Finds the {@link CrashTag} matching given level and tag.
*
* @param level level to find
* @param tag tag to find
* @return the matching {@link CrashTag} or null if no matches exist.
*/
private CrashTag findCrashTag(List<CrashTag> crashTags, String level, String tag) {
for (CrashTag t : crashTags) {
if (t.matches(level, tag)) {
return t;
}
}
return null;
}
/**
* Returns category for a given {@link CrashTag}.
*
* @param level level of the {@link CrashTag}
* @param tag tag of the {@link CrashTag}
* @return category of the {@link CrashTag}, matching search criteria. If nothing was found
* returns {@code JAVA_CRASH}.
*/
private String getJavaCrashCategory(String level, String tag) {
CrashTag crashTag = findCrashTag(mJavaCrashTags, level, tag);
if (crashTag == null) {
return JAVA_CRASH;
}
return crashTag.getCategory();
}
/**
* Class to encapsulate the tags that indicate which crashes should be parsed.
*/
private class CrashTag {
private String mLevel;
private String mTag;
private String mCategory;
public CrashTag(String level, String tag, String category) {
mLevel = level;
mTag = tag;
mCategory = category;
}
public boolean matches(String level, String tag) {
return mLevel.equals(level) && mTag.equals(tag);
}
public String getCategory() {
return mCategory;
}
}
}