1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.loganalysis.parser; 17 18 import com.android.loganalysis.item.LogcatItem; 19 import com.android.loganalysis.item.MiscLogcatItem; 20 import com.android.loganalysis.util.ArrayUtil; 21 import com.android.loganalysis.util.LogPatternUtil; 22 import com.android.loganalysis.util.LogTailUtil; 23 24 import java.io.BufferedReader; 25 import java.io.IOException; 26 import java.text.DateFormat; 27 import java.text.ParseException; 28 import java.text.SimpleDateFormat; 29 import java.util.ArrayList; 30 import java.util.Date; 31 import java.util.HashMap; 32 import java.util.LinkedList; 33 import java.util.List; 34 import java.util.Map; 35 import java.util.regex.Matcher; 36 import java.util.regex.Pattern; 37 38 /** 39 * An {@link IParser} to handle logcat. The parser can handle the time and threadtime logcat 40 * formats. 41 * <p> 42 * Since the timestamps in the logcat do not have a year, the year can be set manually when the 43 * parser is created or through {@link #setYear(String)}. If a year is not set, the current year 44 * will be used. 45 * </p> 46 */ 47 public class LogcatParser implements IParser { 48 public static final String ANR = "ANR"; 49 public static final String JAVA_CRASH = "JAVA_CRASH"; 50 public static final String NATIVE_CRASH = "NATIVE_CRASH"; 51 public static final String HIGH_CPU_USAGE = "HIGH_CPU_USAGE"; 52 public static final String HIGH_MEMORY_USAGE = "HIGH_MEMORY_USAGE"; 53 public static final String RUNTIME_RESTART = "RUNTIME_RESTART"; 54 55 /** 56 * Match a single line of `logcat -v threadtime`, such as: 57 * 58 * <pre>05-26 11:02:36.886 5689 5689 D AndroidRuntime: CheckJNI is OFF.</pre> 59 */ 60 private static final Pattern THREADTIME_LINE = 61 // UID was added to logcat. It could either be a number or a string. 62 Pattern.compile( 63 // timestamp[1] 64 "^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})" 65 // pid/tid and log level [2-4] 66 + "(?:\\s+[0-9A-Za-z]+)?\\s+(\\d+)\\s+(\\d+)\\s+([A-Z])\\s+" 67 // tag and message [5-6] 68 + "(.+?)\\s*: (.*)$"); 69 70 /** 71 * Match a single line of `logcat -v time`, such as: 72 * 06-04 02:32:14.002 D/dalvikvm( 236): GC_CONCURRENT freed 580K, 51% free [...] 73 */ 74 private static final Pattern TIME_LINE = Pattern.compile( 75 "^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})\\s+" + /* timestamp [1] */ 76 "(\\w)/(.+?)\\(\\s*(\\d+)\\): (.*)$"); /* level, tag, pid, msg [2-5] */ 77 78 /** 79 * Match: "*** FATAL EXCEPTION IN SYSTEM PROCESS: message" 80 */ 81 private static final Pattern SYSTEM_SERVER_CRASH = Pattern.compile( 82 "\\*\\*\\* FATAL EXCEPTION IN SYSTEM PROCESS:.*"); 83 /** 84 * Match "Process: com.android.package, PID: 123" or "PID: 123" 85 */ 86 private static final Pattern JAVA_CRASH_PROCESS_PID = Pattern.compile( 87 "^(Process: (\\S+), )?PID: (\\d+)$"); 88 89 90 /** 91 * Match a line thats printed when a non app java process starts. 92 */ 93 private static final Pattern JAVA_PROC_START = Pattern.compile("Calling main entry (.+)"); 94 95 /** 96 * Class for storing logcat meta data for a particular grouped list of lines. 97 */ 98 private class LogcatData { 99 public Integer mPid = null; 100 public Integer mTid = null; 101 public Date mTime = null; 102 public String mLevel = null; 103 public String mTag = null; 104 public String mLastPreamble = null; 105 public String mProcPreamble = null; 106 public List<String> mLines = new LinkedList<String>(); 107 108 public LogcatData(Integer pid, Integer tid, Date time, String level, String tag, 109 String lastPreamble, String procPreamble) { 110 mPid = pid; 111 mTid = tid; 112 mTime = time; 113 mLevel = level; 114 mTag = tag; 115 mLastPreamble = lastPreamble; 116 mProcPreamble = procPreamble; 117 } 118 } 119 120 private LogPatternUtil mPatternUtil = new LogPatternUtil(); 121 private LogTailUtil mPreambleUtil = new LogTailUtil(); 122 123 private String mYear = null; 124 125 LogcatItem mLogcat = null; 126 127 Map<String, LogcatData> mDataMap = new HashMap<String, LogcatData>(); 128 List<LogcatData> mDataList = new LinkedList<LogcatData>(); 129 130 private Date mStartTime = null; 131 private Date mStopTime = null; 132 133 private boolean mIsParsing = true; 134 135 private Map<Integer, String> mPids = new HashMap<Integer, String>(); 136 137 private List<CrashTag> mJavaCrashTags = new ArrayList<>(); 138 private List<CrashTag> mNativeCrashTags = new ArrayList<>(); 139 140 /** 141 * Constructor for {@link LogcatParser}. 142 */ 143 public LogcatParser() { 144 // Add default tag for java crash 145 addJavaCrashTag("E", "AndroidRuntime", JAVA_CRASH); 146 addNativeCrashTag("I", "DEBUG"); 147 addNativeCrashTag("F", "DEBUG"); 148 initPatterns(); 149 } 150 151 /** 152 * Constructor for {@link LogcatParser}. 153 * 154 * @param year The year as a string. 155 */ 156 public LogcatParser(String year) { 157 this(); 158 setYear(year); 159 } 160 161 /** 162 * Sets the year for {@link LogcatParser}. 163 * 164 * @param year The year as a string. 165 */ 166 public void setYear(String year) { 167 mYear = year; 168 } 169 170 /** 171 * Parse a logcat from a {@link BufferedReader} into an {@link LogcatItem} object. 172 * 173 * @param input a {@link BufferedReader}. 174 * @return The {@link LogcatItem}. 175 * @see #parse(List) 176 */ 177 public LogcatItem parse(BufferedReader input) throws IOException { 178 String line; 179 while ((line = input.readLine()) != null) { 180 parseLine(line); 181 } 182 commit(); 183 184 return mLogcat; 185 } 186 187 /** 188 * {@inheritDoc} 189 * 190 * @return The {@link LogcatItem}. 191 */ 192 @Override 193 public LogcatItem parse(List<String> lines) { 194 for (String line : lines) { 195 parseLine(line); 196 } 197 commit(); 198 199 return mLogcat; 200 } 201 202 /** 203 * Clear the existing {@link LogcatItem}. The next parse will create a new one. 204 */ 205 public void clear() { 206 mLogcat = null; 207 mDataList.clear(); 208 mDataMap.clear(); 209 } 210 211 /** 212 * Parse a line of input. 213 * 214 * @param line The line to parse 215 */ 216 private void parseLine(String line) { 217 if ("".equals(line.trim())) { 218 return; 219 } 220 if (mLogcat == null) { 221 mLogcat = new LogcatItem(); 222 } 223 Integer pid = null; 224 Integer tid = null; 225 Date time = null; 226 String level = null; 227 String tag = null; 228 String msg = null; 229 230 Matcher m = THREADTIME_LINE.matcher(line); 231 Matcher tm = TIME_LINE.matcher(line); 232 if (m.matches()) { 233 time = parseTime(m.group(1)); 234 pid = Integer.parseInt(m.group(2)); 235 tid = Integer.parseInt(m.group(3)); 236 level = m.group(4); 237 tag = m.group(5); 238 msg = m.group(6); 239 } else if (tm.matches()) { 240 time = parseTime(tm.group(1)); 241 level = tm.group(2); 242 tag = tm.group(3); 243 pid = Integer.parseInt(tm.group(4)); 244 msg = tm.group(5); 245 } 246 247 if (time != null) { 248 if (mStartTime == null) { 249 mStartTime = time; 250 } 251 mStopTime = time; 252 } 253 254 // Don't parse any lines after device begins reboot until a new log is detected. 255 if ("I".equals(level) && "ShutdownThread".equals(tag) && 256 Pattern.matches("Rebooting, reason: .*", msg)) { 257 mIsParsing = false; 258 } 259 if (Pattern.matches(".*--------- beginning of .*", line)) { 260 mIsParsing = true; 261 } 262 263 if (!mIsParsing || !(m.matches() || tm.matches())) { 264 return; 265 } 266 267 268 // When a non app java process starts add its pid to the map 269 Matcher pidMatcher = JAVA_PROC_START.matcher(msg); 270 if (pidMatcher.matches()) { 271 String name = pidMatcher.group(1); 272 mPids.put(pid, name); 273 } 274 275 // ANRs are separated either by different PID/TIDs or when AnrParser.START matches a line. 276 // The newest entry is kept in the dataMap for quick lookup while all entries are added to 277 // the list. 278 if ("E".equals(level) && "ActivityManager".equals(tag)) { 279 String key = encodeLine(pid, tid, level, tag); 280 LogcatData data; 281 if (!mDataMap.containsKey(key) || AnrParser.START.matcher(msg).matches()) { 282 data = new LogcatData(pid, tid, time, level, tag, mPreambleUtil.getLastTail(), 283 mPreambleUtil.getIdTail(pid)); 284 mDataMap.put(key, data); 285 mDataList.add(data); 286 } else { 287 data = mDataMap.get(key); 288 } 289 data.mLines.add(msg); 290 } 291 292 // Native crashes are separated either by different PID/TIDs or when 293 // NativeCrashParser.FINGERPRINT matches a line. The newest entry is kept in the dataMap 294 // for quick lookup while all entries are added to the list. 295 if (anyNativeCrashTagMatches(level, tag)) { 296 String key = encodeLine(pid, tid, level, tag); 297 LogcatData data; 298 if (!mDataMap.containsKey(key) || NativeCrashParser.FINGERPRINT.matcher(msg).matches()) { 299 data = new LogcatData(pid, tid, time, level, tag, mPreambleUtil.getLastTail(), 300 mPreambleUtil.getIdTail(pid)); 301 mDataMap.put(key, data); 302 mDataList.add(data); 303 } else { 304 data = mDataMap.get(key); 305 } 306 data.mLines.add(msg); 307 } 308 309 // PID and TID are enough to separate Java crashes. 310 if (anyJavaCrashTagMatches(level, tag)) { 311 String key = encodeLine(pid, tid, level, tag); 312 LogcatData data; 313 if (!mDataMap.containsKey(key)) { 314 data = new LogcatData(pid, tid, time, level, tag, mPreambleUtil.getLastTail(), 315 mPreambleUtil.getIdTail(pid)); 316 mDataMap.put(key, data); 317 mDataList.add(data); 318 } else { 319 data = mDataMap.get(key); 320 } 321 data.mLines.add(msg); 322 } 323 324 // Check the message here but add it in commit() 325 if (mPatternUtil.checkMessage(msg, new ExtrasPattern(level, tag)) != null) { 326 LogcatData data = new LogcatData(pid, tid, time, level, tag, 327 mPreambleUtil.getLastTail(), mPreambleUtil.getIdTail(pid)); 328 data.mLines.add(msg); 329 mDataList.add(data); 330 } 331 332 // After parsing the line, add it the the buffer for the preambles. 333 mPreambleUtil.addLine(pid, line); 334 } 335 336 /** 337 * Signal that the input has finished. 338 */ 339 private void commit() { 340 if (mLogcat == null) { 341 return; 342 } 343 for (LogcatData data : mDataList) { 344 MiscLogcatItem item = null; 345 if ("E".equals(data.mLevel) && "ActivityManager".equals(data.mTag)) { 346 item = new AnrParser().parse(data.mLines); 347 } else if (anyJavaCrashTagMatches(data.mLevel, data.mTag)) { 348 // Get the process name/PID from the Java crash, then pass the rest of the lines to 349 // the parser. 350 Integer pid = null; 351 String app = null; 352 for (int i = 0; i < data.mLines.size(); i++) { 353 String line = data.mLines.get(i); 354 Matcher m = JAVA_CRASH_PROCESS_PID.matcher(line); 355 if (m.matches()) { 356 app = m.group(2); 357 pid = Integer.valueOf(m.group(3)); 358 data.mLines = data.mLines.subList(i + 1, data.mLines.size()); 359 break; 360 } 361 m = SYSTEM_SERVER_CRASH.matcher(line); 362 if (m.matches()) { 363 app = mPids.get(data.mPid); 364 if (app == null) { 365 app = "system_server"; 366 } 367 data.mLines = data.mLines.subList(i + 1, data.mLines.size()); 368 break; 369 } 370 } 371 item = new JavaCrashParser().parse(data.mLines); 372 if (item != null) { 373 item.setApp(app); 374 item.setPid(pid); 375 item.setCategory(getJavaCrashCategory(data.mLevel, data.mTag)); 376 } 377 } else if (anyNativeCrashTagMatches(data.mLevel, data.mTag)) { 378 // CLog.v("Parsing native crash: %s", data.mLines); 379 item = new NativeCrashParser().parse(data.mLines); 380 } else { 381 String msg = ArrayUtil.join("\n", data.mLines); 382 String category = mPatternUtil.checkMessage(msg, new ExtrasPattern( 383 data.mLevel, data.mTag)); 384 if (category != null) { 385 MiscLogcatItem logcatItem = new MiscLogcatItem(); 386 logcatItem.setCategory(category); 387 logcatItem.setStack(msg); 388 item = logcatItem; 389 } 390 } 391 if (item != null) { 392 item.setEventTime(data.mTime); 393 if (item.getPid() == null) { 394 item.setPid(data.mPid); 395 item.setTid(data.mTid); 396 } 397 item.setLastPreamble(data.mLastPreamble); 398 item.setProcessPreamble(data.mProcPreamble); 399 item.setTag(data.mTag); 400 mLogcat.addEvent(item); 401 } 402 } 403 404 mLogcat.setStartTime(mStartTime); 405 mLogcat.setStopTime(mStopTime); 406 } 407 408 /** 409 * Create an identifier that "should" be unique for a given logcat. In practice, we do use it as 410 * a unique identifier. 411 */ 412 private static String encodeLine(Integer pid, Integer tid, String level, String tag) { 413 if (tid == null) { 414 return String.format("%d|%s|%s", pid, level, tag); 415 } 416 return String.format("%d|%d|%s|%s", pid, tid, level, tag); 417 } 418 419 /** 420 * Parse the timestamp and return a {@link Date}. If year is not set, the current year will be 421 * used. 422 * 423 * @param timeStr The timestamp in the format {@code MM-dd HH:mm:ss.SSS}. 424 * @return The {@link Date}. 425 */ 426 private Date parseTime(String timeStr) { 427 // If year is null, just use the current year. 428 if (mYear == null) { 429 DateFormat yearFormatter = new SimpleDateFormat("yyyy"); 430 mYear = yearFormatter.format(new Date()); 431 } 432 433 DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); 434 try { 435 return formatter.parse(String.format("%s-%s", mYear, timeStr)); 436 } catch (ParseException e) { 437 // CLog.e("Could not parse time string %s", timeStr); 438 return null; 439 } 440 } 441 442 private void initPatterns() { 443 // High CPU usage 444 mPatternUtil.addPattern(Pattern.compile(".* timed out \\(is the CPU pegged\\?\\).*"), 445 HIGH_CPU_USAGE); 446 447 // High memory usage 448 mPatternUtil.addPattern(Pattern.compile( 449 "GetBufferLock timed out for thread \\d+ buffer .*"), HIGH_MEMORY_USAGE); 450 451 // Runtime restarts 452 mPatternUtil.addPattern(Pattern.compile("\\*\\*\\* WATCHDOG KILLING SYSTEM PROCESS.*"), 453 RUNTIME_RESTART); 454 } 455 456 /** 457 * Adds a custom, more complex pattern to LogcatParser for parsing out of logcat events. 458 * Any matched events are then assigned to the category name provided, and can be grabbed 459 * via LogcatParser's .getMiscEvents("yourCustomCategoryName") method. 460 * Considers null messages, levels, or tags to be wildcards. 461 * 462 * @param pattern The regex representing the pattern to match for, or null for wildcard. 463 * @param level The name of the level to match for, or null for wildcard. 464 * @param tag The name of the tag to match for, or null for wildcard. 465 * @param category Assign any matching logcat events to this category name, for later retrieval 466 */ 467 public void addPattern(Pattern pattern, String level, String tag, String category) { 468 /* count null message as wildcard */ 469 if (pattern == null) { 470 pattern = Pattern.compile(".*"); 471 } 472 mPatternUtil.addPattern(pattern, new ExtrasPattern(level, tag), category); 473 } 474 475 /** 476 * Used internally for bundling up extra pattern criteria for more advanced pattern matching. 477 */ 478 private class ExtrasPattern { 479 public String mLevel; 480 public String mTag; 481 482 public ExtrasPattern(String level, String tag) { 483 mLevel = level; 484 mTag = tag; 485 } 486 487 /** 488 * Override Object.equals to match based on the level & tag patterns, 489 * while also counting null level & tag patterns as wildcards. 490 * 491 * @param otherObj the object we're matching the level & tag patterns to. 492 * @return true if otherObj's extras match, false otherwise 493 */ 494 @Override 495 public boolean equals(Object otherObj) { 496 if (otherObj instanceof ExtrasPattern) { 497 // Treat objects as equal only if the obj's level and tag match. 498 // Treat null as a wildcard. 499 ExtrasPattern other = (ExtrasPattern) otherObj; 500 if ((mLevel == null || other.mLevel == null || mLevel.equals(other.mLevel)) && 501 (mTag == null || other.mTag == null || mTag.equals(other.mTag))) { 502 return true; 503 } 504 } 505 return false; 506 } 507 508 /** {@inheritdoc} */ 509 @Override 510 public int hashCode() { 511 // Since both mLevel and mTag can be wild cards, we can't actually use them to generate 512 // a hashcode without potentially violating the hashcode contract. That doesn't leave 513 // us with anything to actually use to generate the hashcode, so just return a random 514 // static int. 515 return 145800969; 516 } 517 } 518 519 /** 520 * Allows Java crashes to be parsed from multiple log levels and tags. Normally the crashes 521 * are error level messages from AndroidRuntime, but they could also be from other sources. 522 * Use this method to parse java crashes from those other sources. 523 * 524 * @param level log level on which to look for java crashes 525 * @param tag log tag where to look for java crashes 526 */ 527 public void addJavaCrashTag(String level, String tag, String category) { 528 mJavaCrashTags.add(new CrashTag(level, tag, category)); 529 } 530 531 /** 532 * Allows native crashes to be parsed from multiple log levels and tags. The default levels 533 * are "I DEBUG" and "F DEBUG". 534 * 535 * @param level log level on which to look for native crashes 536 * @param tag log tag where to look for native crashes 537 */ 538 private void addNativeCrashTag(String level, String tag) { 539 mNativeCrashTags.add(new CrashTag(level, tag, NATIVE_CRASH)); 540 } 541 542 /** 543 * Determines if any of the Java crash tags is matching a logcat line. 544 * 545 * @param level log level of the logcat line 546 * @param tag tag of the logcat line 547 * @return True if any Java crash tag matches the current level and tag. False otherwise. 548 */ 549 private boolean anyJavaCrashTagMatches(String level, String tag) { 550 return findCrashTag(mJavaCrashTags, level, tag) != null; 551 } 552 553 /** 554 * Determines if any of the native crash tags is matching a logcat line. 555 * 556 * @param level log level of the logcat line 557 * @param tag tag of the logcat line 558 * @return True if any native crash tag matches the current level and tag. False otherwise. 559 */ 560 private boolean anyNativeCrashTagMatches(String level, String tag) { 561 return findCrashTag(mNativeCrashTags, level, tag) != null; 562 } 563 564 /** 565 * Finds the {@link CrashTag} matching given level and tag. 566 * 567 * @param level level to find 568 * @param tag tag to find 569 * @return the matching {@link CrashTag} or null if no matches exist. 570 */ 571 private CrashTag findCrashTag(List<CrashTag> crashTags, String level, String tag) { 572 for (CrashTag t : crashTags) { 573 if (t.matches(level, tag)) { 574 return t; 575 } 576 } 577 return null; 578 } 579 580 /** 581 * Returns category for a given {@link CrashTag}. 582 * 583 * @param level level of the {@link CrashTag} 584 * @param tag tag of the {@link CrashTag} 585 * @return category of the {@link CrashTag}, matching search criteria. If nothing was found 586 * returns {@code JAVA_CRASH}. 587 */ 588 private String getJavaCrashCategory(String level, String tag) { 589 CrashTag crashTag = findCrashTag(mJavaCrashTags, level, tag); 590 if (crashTag == null) { 591 return JAVA_CRASH; 592 } 593 return crashTag.getCategory(); 594 } 595 596 /** 597 * Class to encapsulate the tags that indicate which crashes should be parsed. 598 */ 599 private class CrashTag { 600 private String mLevel; 601 private String mTag; 602 private String mCategory; 603 604 public CrashTag(String level, String tag, String category) { 605 mLevel = level; 606 mTag = tag; 607 mCategory = category; 608 } 609 610 public boolean matches(String level, String tag) { 611 return mLevel.equals(level) && mTag.equals(tag); 612 } 613 614 public String getCategory() { 615 return mCategory; 616 } 617 } 618 } 619