Project

General

Profile

TraceAspect.java

Hynek Cihlar, 06/23/2015 12:28 PM

Download (15.8 KB)

 
1
package com.goldencode.csvtrace;
2
import java.io.*;
3
import java.lang.reflect.Field;
4
import java.nio.file.*;
5
import java.util.*;
6
import java.util.concurrent.atomic.*;
7

    
8
import org.apache.commons.lang.builder.*;
9
import org.aspectj.lang.*;
10
import org.aspectj.lang.annotation.*;
11

    
12
/**
13
 * To use:
14
 * 1. Copy this class to the project at com/goldencode/csvtrace.
15
 * 2. Rebuild with AspectJ.
16
 * 3. When the app is started look for csvtrace.stop in the working dir.
17
 * 4. Modify csvtrace.stop at your liking.
18
 * 5. Rename csvtrace.stop to csvtrace.start to activate tracing.
19
 * 6. Rename csvtrace.start to csvtrace.stop or stop the app to disable tracing.
20
 * 7. The trace output is saved to csvtrace.csv in the working dir.
21
 * 8. When no longer needed, delete this class from your project.
22
 * 
23
 * This thing is WIP.
24
 */
25
@Aspect
26
public class TraceAspect
27
{
28
   private static final String CTRL_FILE_START = "csvtrace.start";
29
   private static final String CTRL_FILE_STOP = "csvtrace.stop";
30
   private static final String CSV_FILE = "csvtrace.csv";
31

    
32
   private static boolean traceActive = false;
33
   private static AtomicReference<List<String>> includes = new AtomicReference<>();
34
   private static AtomicReference<List<String>> excludes = new AtomicReference<>();
35
   private static BufferedWriter csvFileWriter = null;
36

    
37
   static
38
   {
39
      start();
40
   }
41

    
42
   @Before("target(o) && execution(* com.goldencode..*(..))&& !cflow(within(TraceAspect))")
43
   public void traceMethods(JoinPoint joinPoint, Object o)
44
   {
45
      if (!traceActive)
46
      {
47
         return;
48
      }
49
      
50
      List<String> incs = includes.get();
51
      if (incs == null || incs.isEmpty())
52
      {
53
         return;
54
      }
55
      
56
      List<String> excs = excludes.get();
57
      
58
      Signature sig = joinPoint.getStaticPart().getSignature();
59
      Class<?> declaring = sig.getDeclaringType();
60
      String declaringName = declaring.getCanonicalName();
61
      if (declaringName == null)
62
      {
63
         return;
64
      }
65
      
66
      // qualified class name + method name
67
      declaringName = declaringName + "." + sig.getName();
68

    
69
      // match the declaring class against the list of excludes
70
      boolean isMatch = false;
71
      for (String exclude : excs)
72
      {
73
         if (exclude.endsWith("*"))
74
         {
75
            if (declaringName.startsWith(exclude.substring(0, exclude.length() - 1)))
76
            {
77
               isMatch = true;
78
               break;
79
            }
80
         }
81
         else
82
         {
83
            if (declaringName.equals(exclude))
84
            {
85
               isMatch = true;
86
               break;
87
            }
88
         }
89
      }
90
      
91
      if (isMatch)
92
      {
93
         // exclude matched - exit
94
         return;
95
      }
96

    
97
      // match the declaring class against the list of includes
98
      for (String include : incs)
99
      {
100
         if (include.endsWith("*"))
101
         {
102
            if (declaringName.startsWith(include.substring(0, include.length() - 1)))
103
            {
104
               isMatch = true;
105
               break;
106
            }
107
         }
108
         else
109
         {
110
            if (declaringName.equals(include))
111
            {
112
               isMatch = true;
113
               break;
114
            }
115
         }
116
      }
117
      
118
      if (!isMatch)
119
      {
120
         return;
121
      }
122

    
123
      Object[] args = joinPoint.getArgs();
124

    
125
      createCSVEntry(declaringName, o, args);
126
   }
127

    
128
   public void createCSVEntry(String declaringName, Object ref, Object[] args)
129
   {
130
      long nanos = System.nanoTime();
131
      StringBuilder str = new StringBuilder();
132
      str.append(nanos).append(",");
133
      str.append(Thread.currentThread().getName()).append(",");
134
      
135
      if (ref != null)
136
      {
137
         str.append(declaringName).append("(")
138
            .append(ref.getClass().getSimpleName())
139
            .append(")").append(",");
140
      }
141
      else
142
      {
143
         str.append(declaringName).append(",");
144
      }
145
      
146
      str.append("@");
147
      if (ref == null)
148
      {
149
         // do the same length as a valid hash code for easier grep
150
         str.append("00000000");
151
      }
152
      else
153
      {
154
         str.append(Integer.toHexString(System.identityHashCode(ref)));
155
      }
156
      
157
      str.append(",");
158

    
159
      // serialize the arguments
160
      for (Object arg : args)
161
      {
162
         String argStr;
163
         if (arg != null)
164
         {
165
            Class<?> argClass = arg.getClass();
166
            if (isPrimitiveOrWrapper(argClass))
167
            {
168
               argStr = arg.toString();
169
            }
170
            else
171
            {
172
               argStr = toStringReflective(arg, argClass);
173
            }
174
         }
175
         else
176
         {
177
            argStr = "<null>";
178
         }
179

    
180
         // escape control chars
181
         argStr = argStr.replace("\"", "\"\"");
182
         argStr = "\"" + argStr + "\"";
183

    
184
         str.append(argStr).append(",");
185
      }
186

    
187
      String out = str.toString();
188
      synchronized (this)
189
      {
190
         try
191
         {
192
            csvFileWriter.write(out);
193
            csvFileWriter.newLine();
194
            csvFileWriter.flush();
195
         }
196
         catch (IOException e)
197
         {
198
            e.printStackTrace();
199
         }
200
      }
201
   }
202

    
203
   private static void start()
204
   {
205
      String workDir = System.getProperty("user.dir");
206
      Path path = FileSystems.getDefault().getPath(workDir, CSV_FILE);
207
      File csvFile = path.toFile();
208
      try
209
      {
210
         boolean writeColumns = false;
211
         if (!csvFile.exists())
212
         {
213
            writeColumns = true;
214
         }
215
         
216
         csvFileWriter = new BufferedWriter(new FileWriter(csvFile, true));
217
         
218
         if (writeColumns)
219
         {
220
            csvFileWriter.write("Nanos, Thread, Method, Hashcode, Arg1, Arg2, Arg3, Arg4, Arg5");
221
            csvFileWriter.newLine();
222
            csvFileWriter.flush();
223
         }
224
      }
225
      catch (IOException e1)
226
      {
227
         e1.printStackTrace();
228
         return;
229
      }
230

    
231
      path = FileSystems.getDefault().getPath(workDir, CTRL_FILE_START);
232
      File startFile = path.toFile();
233
      path = FileSystems.getDefault().getPath(workDir, CTRL_FILE_STOP);
234
      File stopFile = path.toFile();
235
      if (!startFile.exists() && !stopFile.exists())
236
      {
237
         // create the default file
238
         try (BufferedWriter writer = new BufferedWriter(new FileWriter(stopFile)))
239
         {
240
            writer.write("# rename this file to csvtrace.start to start tracing, or csvtrace.stop to stop tracing");
241
            writer.newLine();
242
            writer.write("# my.name* - all methods of all classes in all the namespaces beginning with 'my.name'");
243
            writer.newLine();
244
            writer.write("# my.namespace.* - all methods of all classes in the namespace 'my.namespace' including subspaces");
245
            writer.newLine();
246
            writer.write("# my.namespace.MyClass* - all methods of class names beginning with 'my.namespace.MyClass'");
247
            writer.newLine();
248
            writer.write("# my.namespace.MyClass.* - all methods of class name equal to 'my.namespace.MyClass'");
249
            writer.newLine();
250
            writer.write("# my.namespace.MyClass.foo* - all methods of class 'my.namespace.MyClass' beginning with 'foo'");
251
            writer.newLine();
252
            writer.write("# my.namespace.MyClass.fooBar - the method 'fooBar' of class 'my.namespace.MyClass'");
253
            writer.newLine();
254
            writer.write("!java.*");
255
            writer.newLine();
256
            writer.write("!javax.*");
257
            writer.newLine();
258
            writer.write("# make sure to apply some filters in the form above and remove the asterisk");
259
            writer.write("*");
260
         }
261
         catch (IOException e)
262
         {
263
            e.printStackTrace();
264
            throw new RuntimeException(e);
265
         }
266
      }
267

    
268
      updateState();
269

    
270
      Thread fileWatcherThread = new Thread(new Runnable()
271
      {
272

    
273
         @Override
274
         public void run()
275
         {
276
            controlFileWatcher();
277
         }
278
      }, "csv-trace");
279

    
280
      fileWatcherThread.setDaemon(true);
281
      fileWatcherThread.start();
282
   }
283

    
284
   private static void controlFileWatcher()
285
   {
286
      WatchService watcher;
287
      try
288
      {
289
         watcher = FileSystems.getDefault().newWatchService();
290
      }
291
      catch (IOException e)
292
      {
293
         e.printStackTrace();
294
         return;
295
      }
296

    
297
      String workDir = System.getProperty("user.dir");
298
      Path dir = FileSystems.getDefault().getPath(workDir);
299

    
300
      try
301
      {
302
         dir.register(watcher, StandardWatchEventKinds.ENTRY_CREATE,
303
               StandardWatchEventKinds.ENTRY_MODIFY);
304
      }
305
      catch (IOException x)
306
      {
307
         System.err.println(x);
308
      }
309

    
310
      for (;;)
311
      {
312

    
313
         // wait for key to be signaled
314
         WatchKey key;
315
         try
316
         {
317
            key = watcher.take();
318
         }
319
         catch (InterruptedException x)
320
         {
321
            return;
322
         }
323

    
324
         for (WatchEvent<?> event : key.pollEvents())
325
         {
326
            WatchEvent.Kind<?> kind = event.kind();
327

    
328
            if (kind == StandardWatchEventKinds.OVERFLOW)
329
            {
330
               continue;
331
            }
332

    
333
            @SuppressWarnings("unchecked")
334
            WatchEvent<Path> ev = (WatchEvent<Path>) event;
335
            Path filename = ev.context();
336

    
337
            if (filename.endsWith(CTRL_FILE_START) || filename.endsWith(CTRL_FILE_STOP))
338
            {
339
               updateState();
340
            }
341
         }
342
         
343
         try
344
         {
345
            // sleep not to consume to much CPU in case the csv file is in the same folder as the
346
            // control file
347
            Thread.sleep(1000);
348
         }
349
         catch (InterruptedException e)
350
         {
351
            break;
352
         }
353

    
354
         // reset the key to receive further events
355
         boolean valid = key.reset();
356
         if (!valid)
357
         {
358
            break;
359
         }
360
      }
361
   }
362

    
363
   private static void updateState()
364
   {
365
      String workDir = System.getProperty("user.dir");
366
      Path path = FileSystems.getDefault().getPath(workDir, CTRL_FILE_START);
367
      File startFile = path.toFile();
368
      path = FileSystems.getDefault().getPath(workDir, CTRL_FILE_STOP);
369
      File stopFile = path.toFile();
370

    
371
      if (stopFile.exists())
372
      {
373
         includes.set(null);
374
         return;
375
      }
376

    
377
      if (startFile.exists())
378
      {
379
         processControlFile(startFile);
380
      }
381
   }
382

    
383
   private static void processControlFile(File file)
384
   {
385
      List<String> tempIncludes = new ArrayList<>();
386
      List<String> tempExcludes = new ArrayList<>();
387

    
388
      try (BufferedReader br = new BufferedReader(new FileReader(file)))
389
      {
390
         String line = null;
391
         while ((line = br.readLine()) != null)
392
         {
393
            line = line.trim();
394
            if (line.startsWith("#"))
395
            {
396
               continue;
397
            }
398

    
399
            // anything else than # is considered an include pattern
400

    
401
            line = line.trim();
402

    
403
            if (line.startsWith("!"))
404
            {
405
               tempExcludes.add(line.substring(1));
406
            }
407
            else
408
            {
409
               tempIncludes.add(line.trim());
410
            }
411
         }
412
      }
413
      catch (IOException e)
414
      {
415
         e.printStackTrace();
416
      }
417

    
418
      excludes.set(tempExcludes);
419
      includes.set(tempIncludes);
420
      traceActive = !tempIncludes.isEmpty();
421
   }
422
   
423
   private static boolean isPrimitiveOrWrapper(Class<?> clazz)
424
   {
425
      return clazz.isPrimitive() || 
426
             clazz.equals(Boolean.class) ||
427
             clazz.equals(Character.class) ||
428
             clazz.equals(Short.class) ||
429
             clazz.equals(Integer.class) ||
430
             clazz.equals(Long.class) ||
431
             clazz.equals(Float.class) ||
432
             clazz.equals(Double.class) ||
433
             CharSequence.class.isAssignableFrom(clazz);
434
   }
435
   
436
   private static String toStringReflective(Object value, Class<?> clazz)
437
   {
438
      if (value == null)
439
      {
440
         return "<null>";
441
      }
442
      
443
      if (isPrimitiveOrWrapper(clazz))
444
      {
445
         return value.toString();
446
      }
447
      
448
      StringBuilder b = new StringBuilder();
449
      b.append(clazz.getSimpleName());
450
      b.append("@");
451
      b.append(System.identityHashCode(value));
452
      
453
      b.append("[");
454
      
455
      boolean first = true;
456
      Class<?> parent = clazz;
457
      while(parent != null && !parent.getClass().equals(Object.class))
458
      {
459
         Field[] fields = parent.getDeclaredFields();
460
         for(Field f : fields)
461
         {
462
            String fName = f.getName();
463
            if (fName.startsWith("ajc$"))
464
            {
465
               // don't care for aspectj internals
466
               continue;
467
            }
468
            
469
            if (!first)
470
            {
471
               b.append(",");
472
            }
473
            first = false;
474
            f.setAccessible(true);
475
            b.append(fName);
476
            b.append("=");
477
            Object fValue;
478
            try
479
            {
480
               fValue = f.get(value);
481
               if (fValue == null)
482
               {
483
                  b.append("<null>");
484
               }
485
               else if (isPrimitiveOrWrapper(f.getType()))
486
               {
487
                  b.append(fValue.toString());
488
               }
489
               else
490
               {
491
                  b.append("[" + fValue.toString() + "]");
492
               }
493
            }
494
            catch (Exception e)
495
            {
496
               b.append("<error>");
497
            }
498
         }
499
         parent = parent.getSuperclass();
500
      }
501
      
502
      b.append("]");
503
      return b.toString();
504
   }
505
   
506
   @SuppressWarnings("serial")
507
   private static class TwoLevelsToStringStyle extends ToStringStyle
508
   {
509

    
510
      private static final int MAX_DEPTH = 1;
511

    
512
      private int depth = 0;
513

    
514
      /**
515
       * <p>
516
       * Constructor.
517
       * </p>
518
       */
519
      public TwoLevelsToStringStyle()
520
      {
521
         super();
522
         this.setUseShortClassName(true);
523
         this.setUseIdentityHashCode(true);
524

    
525
      }
526

    
527
      @Override
528
      public void appendDetail(StringBuffer buffer, String fieldName, Object value)
529
      {
530
         if (depth > MAX_DEPTH)
531
         {
532
            return;
533
         }
534
         
535
         Class<?> valueClass = value.getClass();
536
         
537
         if (valueClass.isPrimitive() || 
538
               valueClass.equals(Boolean.class) ||
539
               valueClass.equals(Character.class) ||
540
               valueClass.equals(Short.class) ||
541
               valueClass.equals(Integer.class) ||
542
               valueClass.equals(Long.class) ||
543
               valueClass.equals(Float.class) ||
544
               valueClass.equals(Double.class) ||
545
               CharSequence.class.isAssignableFrom(valueClass))
546
         {
547
            super.appendDetail(buffer, fieldName, value);
548
         }
549
         else if (accept(valueClass))
550
         {
551
            depth++;
552
            try
553
            {
554
               buffer.append(ToStringBuilder.reflectionToString(value, this));
555
            }
556
            finally
557
            {
558
               depth--;
559
            }
560

    
561
         }
562
      }
563
      
564

    
565
//      @Override
566
//      protected void appendDetail(StringBuffer buffer, String fieldName, Collection<?> coll)
567
//      {
568
//         if (depth > MAX_DEPTH)
569
//         {
570
//            return;
571
//         }
572
   //
573
//         appendClassName(buffer, coll);
574
//         appendIdentityHashCode(buffer, coll);
575
//         appendDetail(buffer, fieldName, coll.toArray());
576
//      }
577

    
578
      /**
579
       * Returns whether or not to recursively format the given <code>Class</code>. By default, this
580
       * method always returns {@code true}, but may be overwritten by sub-classes to filter specific
581
       * classes.
582
       *
583
       * @param clazz
584
       *           The class to test.
585
       * @return Whether or not to recursively format the given <code>Class</code>.
586
       */
587
      protected boolean accept(final Class<?> clazz)
588
      {
589
         // TODO: WeakHasMap is internally used in ToStringBuilder implementation 
590
         //       and causes infinite recursion 
591
         return !WeakHashMap.class.isAssignableFrom(clazz);
592
      }
593
   }
594
}