summaryrefslogtreecommitdiff
path: root/src/com/android/loganalysis/parser/LogcatParser.java
blob: b32536da549defb54232cf277b6fff38f9b3fec5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
/*
 * 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:
     *
     * <pre>05-26 11:02:36.886 5689 5689 D AndroidRuntime: CheckJNI is OFF.</pre>
     */
    private static final Pattern THREADTIME_LINE =
            // UID was added to logcat. It could either be a number or a string.
            Pattern.compile(
                    // timestamp[1]
                    "^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})"
                            // pid/tid and log level [2-4]
                            + "(?:\\s+[0-9A-Za-z]+)?\\s+(\\d+)\\s+(\\d+)\\s+([A-Z])\\s+"
                            // tag and message [5-6]
                            + "(.+?)\\s*: (.*)$");

    /**
     * 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;
        }

        /** {@inheritdoc} */
        @Override
        public int hashCode() {
            // Since both mLevel and mTag can be wild cards, we can't actually use them to generate
            // a hashcode without potentially violating the hashcode contract. That doesn't leave
            // us with anything to actually use to generate the hashcode, so just return a random
            // static int.
            return 145800969;
        }
    }

    /**
     * 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;
        }
    }
}