1 /*******************************************************************************
2 * Copyright (c) 2016 Ericsson
4 * All rights reserved. This program and the accompanying materials are
5 * made available under the terms of the Eclipse Public License v1.0 which
6 * accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
8 *******************************************************************************/
10 package org
.eclipse
.tracecompass
.common
.core
.log
;
12 import java
.util
.Arrays
;
13 import java
.util
.HashMap
;
14 import java
.util
.HashSet
;
15 import java
.util
.Iterator
;
17 import java
.util
.Map
.Entry
;
19 import java
.util
.concurrent
.atomic
.AtomicInteger
;
20 import java
.util
.logging
.Level
;
21 import java
.util
.logging
.Logger
;
23 import org
.eclipse
.jdt
.annotation
.Nullable
;
28 * This is a logger helper, it will allow entry-exit analysis to be much easier.
30 * The events are saved in a JSON-like message in the phase of the event. It is
31 * an event type but with extra information associated to it. Typical types can
36 * <li><strong>B</strong>, Begin</li>
37 * <li><strong>E</strong>, End</li>
38 * <li><strong>X</strong>, Complete, this is an event with a duration field</li>
39 * <li><strong>i</strong>, Instant / Info</li>
42 * <li>Asynchronous nested messages
44 * <li><strong>b</strong>, nested begin</li>
45 * <li><strong>n</strong>, nested info</li>
46 * <li><strong>e</strong>, nested end</li>
51 * <li><strong>s</strong>, flow begin</li>
52 * <li><strong>t</strong>, flow step (info)</li>
53 * <li><strong>f</strong>, flow end</li>
58 * <li><strong>N</Strong>, Object created</li>
59 * <li><strong>D</Strong>, Object destroyed</li>
62 * <li>Mark Events - events that generate markers
64 * <li><strong>R</strong>, Marker event</li>
67 * <li>CounterEvents - events that count items
69 * <li><strong>C</strong>, Counter event</li>
74 * To use <strong>durations</strong> and/or <strong>flows</strong>, see
75 * {@link ScopeLog} and {@link FlowScopeLog}. These 2 concepts are related.
76 * Durations would typically be used to instrument simple methods, while flows
77 * would be preferred if there are links to be made with other threads.
79 * To use <strong>Asynchronous nested messages</strong>, see
80 * {@link #traceAsyncStart(Logger, Level, String, String, int, Object...)}, and
81 * {@link #traceAsyncEnd(Logger, Level, String, String, int, Object...)}
83 * To use <strong>Object tracking</strong>, see
84 * {@link #traceObjectCreation(Logger, Level, Object)} and
85 * {@link #traceObjectDestruction(Logger, Level, Object)}
87 * The design philosophy of this class is very heavily inspired by the trace
88 * event format of Google. The full specification is available <a
89 * href=https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/edit?pli=1#>here</a>.
92 * The main goals are clarity of output and simplicity for the developer.
93 * Performance is a nice to have, but is not the main concern of this helper. A
94 * minor performance impact compared to simply logging the events is to be
97 * @author Matthew Khouzam
99 * @noinstantiate This class is not intended to be instantiated by clients. It
102 public final class TraceCompassLogUtils
{
107 private static final String ARGS
= "args"; //$NON-NLS-1$
108 private static final String NAME
= "name"; //$NON-NLS-1$
109 private static final String CATEGORY
= "cat"; //$NON-NLS-1$
110 private static final String ID
= "id"; //$NON-NLS-1$
111 private static final String TID
= "tid"; //$NON-NLS-1$
112 private static final String TIMESTAMP
= "ts"; //$NON-NLS-1$
113 private static final String PHASE
= "ph"; //$NON-NLS-1$
115 private static final String ARGS_ERROR_MESSAGE
= "Data should be in the form of key, value, key1, value1, ... TraceCompassScopeLog was supplied "; //$NON-NLS-1$
116 private static final AtomicInteger ID_GENERATOR
= new AtomicInteger(0);
118 private TraceCompassLogUtils() {
123 * Scope Logger helper. This will automatically log entry and exit of the
124 * scope. This scope log will be shown under any scope enclosing it, but
125 * will not be the source, or destination of any link to other scopes. If
126 * relations should be done with other scopes, the {@link FlowScopeLog}
127 * class is more appropriate.
132 * {@code usage of ScopeLog}
133 * try (ScopeLog linksLogger = new ScopeLog(LOGGER, Level.CONFIG, "Perform Query")) { //$NON-NLS-1$
134 * ss.updateAllReferences();
135 * dataStore.addAll(ss.query(ts, trace));
139 * will generate the following trace
142 * {@code trace output}
143 * INFO: {"ts":12345,"ph":"B",tid:1,"name:Perform Query"}
144 * INFO: {"ts":"12366,"ph":"E","tid":1}
147 public static class ScopeLog
implements AutoCloseable
{
149 private final long fThreadId
;
150 private final Logger fLogger
;
151 private final Level fLevel
;
152 private final Map
<String
, Object
> fData
= new HashMap
<>();
155 * Scope logger constructor
158 * the JUL logger to log to
160 * the log level see {@link Level}
162 * The label of the event pair
164 * Additional messages to pass for this scope, should be in
165 * pairs key, value, key2, value2.... typically arguments.
166 * Note that these arguments will be logged only at the
167 * beginning of the scope
169 public ScopeLog(Logger log
, Level level
, String label
, Object
... args
) {
170 long time
= System
.nanoTime();
173 fThreadId
= Thread
.currentThread().getId();
174 fLogger
.log(fLevel
, (() -> {
175 StringBuilder sb
= new StringBuilder();
177 appendCommon(sb
, 'B', time
, fThreadId
);
178 appendName(sb
, label
);
179 appendArgs(sb
, args
);
181 return sb
.toString();
186 * Add a tag to the scope logger, will be written at the exit. This can
187 * save space on the trace by having a field appended to an event rather
188 * than writing a whole new event for a small chunk of data.
190 * If the timing information is important than it would be more
191 * appropriate to call
192 * {@link TraceCompassLogUtils#traceInstant(Logger, Level, String, Object...)}
195 * the name of the field
197 * The value of the field.
199 public void addData(String name
, Object value
) {
200 fData
.put(name
, value
);
204 public void close() {
205 long time
= System
.nanoTime();
206 fLogger
.log(fLevel
, (() -> {
207 StringBuilder sb
= new StringBuilder();
209 appendCommon(sb
, 'E', time
, fThreadId
);
210 return appendArgs(sb
, fData
).append('}').toString();
216 * Builder class for the {@link FlowScopeLog}. One can either set a category
217 * or a parent scope before building the flow scope log. If none is set, a
218 * default category called "null" will be used.
220 * @author Geneviève Bastien
222 public static class FlowScopeLogBuilder
{
224 private final Logger fLogger
;
225 private final Level fLevel
;
226 private final String fLabel
;
227 private final Object
[] fArgs
;
228 private @Nullable String fCategory
= null;
229 private @Nullable FlowScopeLog fParent
= null;
232 * Flow scope log builder constructor
237 * the log level see {@link Level}
239 * The label of the event pair
241 * the messages to pass, should be in pairs key, value, key2,
242 * value2.... typically arguments
244 public FlowScopeLogBuilder(Logger logger
, Level level
, String label
, Object
... args
) {
252 * Set a category for the flow scope. When building the scope, an ID
253 * will be automatically generated.
255 * This method is mutually exclusive with
256 * {@link #setParentScope(FlowScopeLog)}. Calling both will throw an
260 * The category of this flow
261 * @return This builder
263 public FlowScopeLogBuilder
setCategory(String category
) {
264 if (fParent
!= null) {
265 throw new IllegalStateException("FlowScopeLogBuilder: Cannot set a category if a parent has already been set"); //$NON-NLS-1$
267 fCategory
= category
;
272 * Set a parent scope for the flow scope to build. The scope will have
273 * the same category and ID as the parent scope.
275 * This method is mutually exclusive with {@link #setCategory(String)}.
276 * Calling both will throw an exception.
280 * @return This builder
282 public FlowScopeLogBuilder
setParentScope(FlowScopeLog parent
) {
283 if (fCategory
!= null) {
284 throw new IllegalStateException("FlowScopeLogBuilder: Cannot set a parent scope if a category has already been set"); //$NON-NLS-1$
291 * Build the flow scope log
293 * @return The flow scope log
295 public FlowScopeLog
build() {
296 FlowScopeLog parent
= fParent
;
297 if (parent
!= null) {
298 return new FlowScopeLog(fLogger
, fLevel
, fLabel
, parent
.fCategory
, parent
.fId
, fArgs
);
300 return new FlowScopeLog(fLogger
, fLevel
, fLabel
, String
.valueOf(fCategory
), ID_GENERATOR
.incrementAndGet(), fArgs
);
306 * Flow Scope Logger helper. It will automatically log entry and exit of the
307 * scope. It can be used with other flow scopes to follow the program flow
308 * across threads. To do so, these scopes save more data, so take more disk
309 * space. If there is no inter-process/thread communication to follow, the
310 * {@link ScopeLog} class would be more appropriate.
312 * Usage: this can be used to track asynchronous threads communication. This
313 * can be used in scatter-gather/map-reduce operations as well as threads
314 * that trigger a UI Thread operation.
317 * {@code usage of FlowScopeLog}
318 * try (FlowScopeLog linksLogger = new FlowScopeLog(LOGGER, Level.CONFIG, "Perform Query", "category", 0x100)) { //$NON-NLS-1$
319 * Display.asynchExec(()->{
320 * try(FlowScopeLog linksLogger2 = new FlowScopeLog(LOGGER, Level.CONFIG, "Update UI", "category", linksLogger.getId()) {
321 * linksLogger.step("updating ui");
323 * linksLogger.step("forked thread");
327 * will generate the following trace (order not guaranteed)
330 * {@code trace output}
331 * INFO: {"ts":12345,"ph":"s",tid:1,"name":"Perform Query", "cat":"category", "id":256}
332 * INFO: {"ts":12346","ph":"t",tid:1,"name":"forked thread","cat":"category", "id":256}
333 * INFO: {"ts":"12366,"ph":"f","tid":1,"cat":"category", "id":256}
334 * INFO: {"ts":12400,"ph":"s",tid:0,"name":"Update UI","cat":"category", "id":256}
335 * INFO: {"ts":12416","ph":"t",tid:0,"name":"updating ui", "cat":"category", "id":256}
336 * INFO: {"ts":"12420,"ph":"f","tid":0,"cat":"category", "id":256}
339 public static class FlowScopeLog
implements AutoCloseable
{
341 private final long fThreadId
;
342 private final Logger fLogger
;
343 private final Level fLevel
;
344 private final int fId
;
345 private final String fCategory
;
346 private final Map
<String
, Object
> fData
= new HashMap
<>();
349 * Flow scope logger constructor
354 * the log level see {@link Level}
356 * The label of the event pair
358 * the category of the flow events
362 * the messages to pass, should be in pairs key, value, key2,
363 * value2.... typically arguments
365 private FlowScopeLog(Logger log
, Level level
, String label
, String category
, int id
, Object
... args
) {
366 long time
= System
.nanoTime();
370 fCategory
= category
;
371 fThreadId
= Thread
.currentThread().getId();
372 fLogger
.log(fLevel
, (() -> {
373 StringBuilder sb
= new StringBuilder();
375 appendCommon(sb
, 's', time
, fThreadId
);
376 appendName(sb
, label
);
377 appendCategory(sb
, category
);
379 appendArgs(sb
, args
);
381 return sb
.toString();
386 * Flow step, could be launching a new process or simply hit a
390 * The label for this step
392 * the arguments to log
394 public void step(String label
, Object
... args
) {
395 long time
= System
.nanoTime();
396 fLogger
.log(fLevel
, (() -> {
397 StringBuilder sb
= new StringBuilder();
399 appendCommon(sb
, 't', time
, fThreadId
);
400 appendName(sb
, label
);
401 appendCategory(sb
, fCategory
);
403 appendArgs(sb
, args
);
405 return sb
.toString();
410 * Add a tag to the scope logger, will be written at the exit. This can
411 * save space on the trace by having a field appended to an event rather
412 * than writing a whole new event for a small chunk of data.
415 * If the timing information is important, then it would be more
416 * appropriate to call {@link #step(String, Object...)}
419 * the name of the field
421 * The value of the field.
423 public void addData(String name
, Object value
) {
424 fData
.put(name
, value
);
428 public void close() {
429 long time
= System
.nanoTime();
430 fLogger
.log(fLevel
, (() -> {
431 StringBuilder sb
= new StringBuilder();
433 appendCommon(sb
, 'f', time
, fThreadId
);
434 appendCategory(sb
, fCategory
);
436 appendArgs(sb
, fData
);
438 return sb
.toString();
444 * Trace Object Creation, logs the beginning of an object's life cycle.
445 * Typically one can put this in the object's constructor. However if an
446 * object is mutable, it can be tracked through phases with this method,
447 * then the object can be re-used, however, the resulting analyses may yield
448 * erroneous data if precautions are not taken.
450 * For mutable objects, save the return value of the call. This will be
451 * passed to the destruction of the object and then it can be matched.
456 * The {@link Level} of this event.
458 * the Object to trace
459 * @return The unique ID of this object (there may be collisions)
461 public static int traceObjectCreation(Logger logger
, Level level
, Object item
) {
462 long time
= System
.nanoTime();
463 long threadId
= Thread
.currentThread().getId();
464 int identityHashCode
= System
.identityHashCode(item
);
465 logger
.log(level
, () -> {
466 StringBuilder sb
= new StringBuilder();
468 appendCommon(sb
, 'N', time
, threadId
);
469 appendName(sb
, item
.getClass().getSimpleName());
470 appendId(sb
, identityHashCode
);
471 return sb
.append('}').toString();
473 return identityHashCode
;
477 * Trace Object Destruction, logs the end of an object's life cycle.
478 * Typically one can put this in the object's Dispose(). However if an
479 * object is mutable, it can be tracked through phases with this method,
480 * then the object can be re-used, however, the resulting analyses may yield
481 * erroneous data if precautions are not taken.
486 * The {@link Level} of this event.
488 * the Object to trace
490 public static void traceObjectDestruction(Logger logger
, Level level
, Object item
) {
491 long time
= System
.nanoTime();
492 long threadId
= Thread
.currentThread().getId();
493 logger
.log(level
, () -> {
494 StringBuilder sb
= new StringBuilder();
496 appendCommon(sb
, 'D', time
, threadId
);
497 appendName(sb
, item
.getClass().getSimpleName());
498 appendId(sb
, System
.identityHashCode(item
));
499 return sb
.append('}').toString();
504 * Trace Object Destruction, logs the end of an object's life cycle.
505 * Typically one can put this in the object's Dispose(). However if an
506 * object is mutable, it can be tracked through phases with this method,
507 * then the object can be re-used, however, the resulting analyses may be
512 * The {@link Level} of this event.
514 * the Object to trace
518 public static void traceObjectDestruction(Logger logger
, Level level
, Object item
, int uniqueId
) {
519 long time
= System
.nanoTime();
520 long threadId
= Thread
.currentThread().getId();
521 logger
.log(level
, () -> {
522 StringBuilder sb
= new StringBuilder();
524 appendCommon(sb
, 'D', time
, threadId
);
525 appendName(sb
, item
.getClass().getSimpleName());
526 appendId(sb
, uniqueId
);
527 return sb
.append('}').toString();
532 * Asynchronous events are used to specify asynchronous operations, such as
533 * an asynchronous (or synchronous) draw, or a network operation. Call this
534 * method at the beginning of such an operation.
539 * The {@link Level} of this event.
541 * The name of the asynchronous message
543 * the category of the asynchronous event
545 * The unique ID of a transaction
547 * Additional arguments to log
549 public static void traceAsyncStart(Logger logger
, Level level
, @Nullable String name
, @Nullable String category
, int id
, Object
... args
) {
550 long time
= System
.nanoTime();
551 long threadId
= Thread
.currentThread().getId();
552 logger
.log(level
, () -> {
553 StringBuilder sb
= new StringBuilder();
555 appendCommon(sb
, 'b', time
, threadId
);
556 appendName(sb
, name
);
557 appendCategory(sb
, category
);
559 return appendArgs(sb
, args
).append('}').toString();
564 * Asynchronous events are used to specify asynchronous operations, such as
565 * an asynchronous (or synchronous) draw, or a network operation. Call this
566 * method to augment the asynchronous event with nested information.
571 * The {@link Level} of this event.
573 * The name of the asynchronous message
575 * the category of the asynchronous event
577 * The unique ID of a transaction
579 * Additional arguments to log
581 public static void traceAsyncNested(Logger logger
, Level level
, @Nullable String name
, @Nullable String category
, int id
, Object
... args
) {
582 long time
= System
.nanoTime();
583 long threadId
= Thread
.currentThread().getId();
584 logger
.log(level
, () -> {
585 StringBuilder sb
= new StringBuilder();
587 appendCommon(sb
, 'n', time
, threadId
);
588 appendName(sb
, name
);
589 appendCategory(sb
, category
);
591 return appendArgs(sb
, args
).append('}').toString();
596 * Asynchronous events are used to specify asynchronous operations, such as
597 * an asynchronous (or synchronous) draw, or a network operation. Call this
598 * method at the end of such an operation.
603 * The {@link Level} of this event.
605 * The name of the asynchronous message
607 * the category of the asynchronous event
609 * The unique ID of a transaction
611 * Additional arguments to log
613 public static void traceAsyncEnd(Logger logger
, Level level
, @Nullable String name
, @Nullable String category
, int id
, Object
... args
) {
614 long time
= System
.nanoTime();
615 long threadId
= Thread
.currentThread().getId();
616 logger
.log(level
, () -> {
617 StringBuilder sb
= new StringBuilder();
619 appendCommon(sb
, 'e', time
, threadId
);
620 appendName(sb
, name
);
621 appendCategory(sb
, category
);
623 return appendArgs(sb
, args
).append('}').toString();
628 * Instant events, created to indicate an item of interest has occurred,
629 * similar to a standard System.out.println() or a
630 * Java.util.Logger#log(Level). This one provides an event in a more
631 * structured way. This should be the method to call to save data that
632 * should have a zero duration, as it will ensure a log format that can then
633 * be parsed by a trace type.
638 * The {@link Level} of this event.
640 * The name of the asynchronous message
642 * Additional arguments to log
644 public static void traceInstant(Logger logger
, Level level
, String name
, Object
... args
) {
645 long time
= System
.nanoTime();
646 long threadId
= Thread
.currentThread().getId();
647 logger
.log(level
, () -> {
648 StringBuilder sb
= new StringBuilder();
650 appendCommon(sb
, 'i', time
, threadId
);
651 appendName(sb
, name
);
652 return appendArgs(sb
, args
).append('}').toString();
657 * The counter events can track a value or multiple values as they change
663 * The {@link Level} of this event.
665 * The name of the asynchronous message
667 * The counters to log in the format : "title", value
669 public static void traceCounter(Logger logger
, Level level
, @Nullable String name
, Object
... args
) {
670 long time
= System
.nanoTime();
671 long threadId
= Thread
.currentThread().getId();
672 logger
.log(level
, () -> {
673 StringBuilder sb
= new StringBuilder();
675 appendCommon(sb
, 'C', time
, threadId
);
676 appendName(sb
, name
);
677 return appendArgs(sb
, args
).append('}').toString();
681 // -------------------------------------------------------------------------
683 // -------------------------------------------------------------------------
688 private static StringBuilder
appendCommon(StringBuilder appendTo
, char phase
, long time
, long threadId
) {
689 writeObject(appendTo
, TIMESTAMP
, time
).append(','); // $NON-NLS-1$
690 writeObject(appendTo
, PHASE
, phase
).append(',');
691 return writeObject(appendTo
, TID
, threadId
); // $NON-NLS-1$
694 private static StringBuilder
appendName(StringBuilder sb
, @Nullable String name
) {
697 writeObject(sb
, NAME
, name
);
702 private static StringBuilder
appendCategory(StringBuilder sb
, @Nullable String category
) {
703 if (category
!= null) {
705 writeObject(sb
, CATEGORY
, category
);
710 private static StringBuilder
appendId(StringBuilder sb
, int id
) {
711 return sb
.append(',')
714 .append("\":\"0x") //$NON-NLS-1$
715 .append(Integer
.toHexString(id
))
719 private static StringBuilder
appendArgs(StringBuilder sb
, Map
<String
, Object
> args
) {
720 if (!args
.isEmpty()) {
726 Object
[] argsArray
= new Object
[2 * args
.size()];
727 Iterator
<Entry
<String
, Object
>> entryIter
= args
.entrySet().iterator();
728 for (int i
= 0; i
< args
.size(); i
++) {
729 Entry
<String
, Object
> entry
= entryIter
.next();
730 argsArray
[i
] = entry
.getKey();
731 argsArray
[i
+ 1] = entry
.getValue();
733 getArgs(sb
, argsArray
);
738 private static StringBuilder
appendArgs(StringBuilder sb
, Object
... args
) {
739 if (args
.length
> 0) {
750 private static StringBuilder
getArgs(StringBuilder appendTo
, Object
[] data
) {
751 if (data
.length
== 0) {
754 Set
<String
> tester
= new HashSet
<>();
755 appendTo
.append('{');
756 if (data
.length
== 1) {
757 // not in contract, but let's assume here that people are still new
759 appendTo
.append("\"msg\":\"").append(data
[0]).append('"'); //$NON-NLS-1$
761 if (data
.length
% 2 != 0) {
762 throw new IllegalArgumentException(
763 ARGS_ERROR_MESSAGE
+ "an odd number of messages" + Arrays
.asList(data
).toString()); //$NON-NLS-1$
765 for (int i
= 0; i
< data
.length
- 1; i
+= 2) {
766 Object value
= data
[i
+ 1];
767 String keyVal
= String
.valueOf(data
[i
]);
768 if (tester
.contains(keyVal
)) {
769 throw new IllegalArgumentException(ARGS_ERROR_MESSAGE
+ "an duplicate field names : " + keyVal
); //$NON-NLS-1$
773 appendTo
.append(',');
775 writeObject(appendTo
, keyVal
, value
);
779 return appendTo
.append('}');
782 private static StringBuilder
writeObject(StringBuilder appendTo
, Object key
, @Nullable Object value
) {
783 appendTo
.append('"').append(key
).append('"').append(':');
784 if (value
instanceof Number
) {
785 appendTo
.append(value
);
787 appendTo
.append('"').append(String
.valueOf(value
)).append('"');