    001/*
    002 * Copyright (c) 2016 Chris K Wensel <chris@wensel.net>. All Rights Reserved.
    003 * Copyright (c) 2007-2017 Xplenty, Inc. All Rights Reserved.
    004 *
    005 * Project and contact information: http://www.cascading.org/
    006 *
    007 * This file is part of the Cascading project.
    008 *
    009 * Licensed under the Apache License, Version 2.0 (the "License");
    010 * you may not use this file except in compliance with the License.
    011 * You may obtain a copy of the License at
    012 *
    013 *     http://www.apache.org/licenses/LICENSE-2.0
    014 *
    015 * Unless required by applicable law or agreed to in writing, software
    016 * distributed under the License is distributed on an "AS IS" BASIS,
    017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    018 * See the License for the specific language governing permissions and
    019 * limitations under the License.
    020 */
    021
    022package cascading.util;
    023
    024import java.io.IOException;
    025import java.io.LineNumberReader;
    026import java.io.PrintWriter;
    027import java.io.StringReader;
    028import java.io.StringWriter;
    029import java.io.Writer;
    030import java.util.ArrayList;
    031import java.util.Collection;
    032import java.util.List;
    033import java.util.Map;
    034import java.util.concurrent.ConcurrentHashMap;
    035import java.util.regex.Pattern;
    036
    037import cascading.flow.FlowElement;
    038import cascading.operation.BaseOperation;
    039import cascading.operation.Operation;
    040import cascading.pipe.Pipe;
    041import cascading.scheme.Scheme;
    042import cascading.tap.Tap;
    043
    044/**
    045 *
    046 */
    047public class TraceUtil
    048  {
    049  /**
    050   * The set of regex patterns specifying fully qualified class or package names that serve as boundaries
    051   * for collecting traces and apiCalls in captureDebugTraceAndApiCall.
    052   */
    053  private static final Map<String, Pattern> registeredApiBoundaries = new ConcurrentHashMap<String, Pattern>();
    054
    055  private static interface TraceFormatter
    056    {
    057    String format( String trace );
    058    }
    059
    060  /**
    061   * Add a regex that serves as a boundary for tracing. That is to say any trace
    062   * captured by {@link #captureDebugTrace(Object)} will be from a caller that comes higher in the
    063   * stack than any apiBoundary package or class.
    064   *
    065   * @param apiBoundary
    066   */
    067  public static void registerApiBoundary( String apiBoundary )
    068    {
    069    registeredApiBoundaries.put( apiBoundary, Pattern.compile( apiBoundary ) );
    070    }
    071
    072  /**
    073   * Remove a regex as a boundary for tracing. See registerApiBoundary.
    074   *
    075   * @param apiBoundary
    076   */
    077  public static void unregisterApiBoundary( String apiBoundary )
    078    {
    079    registeredApiBoundaries.remove( apiBoundary );
    080    }
    081
    082  /**
    083   * Allows for custom trace fields on Pipe, Tap, and Scheme types
    084   *
    085   * @param object
    086   * @param trace
    087   */
    088  public static void setTrace( Object object, String trace )
    089    {
    090    Util.setInstanceFieldIfExists( object, "trace", trace );
    091    }
    092
    093  private static String formatTrace( Traceable traceable, String message, TraceFormatter formatter )
    094    {
    095    if( traceable == null )
    096      return message;
    097
    098    String trace = traceable.getTrace();
    099
    100    if( trace == null )
    101      return message;
    102
    103    return formatter.format( trace ) + " " + message;
    104    }
    105
    106  public static String formatTraces( Collection<FlowElement> flowElements, String delim )
    107    {
    108    List<String> messages = new ArrayList<>( flowElements.size() );
    109
    110    for( FlowElement flowElement : flowElements )
    111      messages.add( formatTrace( (Traceable) flowElement, flowElement.toString(), new TraceFormatter()
    112        {
    113        @Override
    114        public String format( String trace )
    115          {
    116          return "[" + trace + "] ->";
    117          }
    118        } ) );
    119
    120    return Util.join( messages, delim );
    121    }
    122
    123  public static String formatTrace( final Scheme scheme, String message )
    124    {
    125    return formatTrace( scheme, message, new TraceFormatter()
    126      {
    127      @Override
    128      public String format( String trace )
    129        {
    130        return "[" + Util.truncate( scheme.toString(), 25 ) + "][" + trace + "]";
    131        }
    132      } );
    133    }
    134
    135  public static String formatTrace( FlowElement flowElement, String message )
    136    {
    137    if( flowElement == null )
    138      return message;
    139
    140    if( flowElement instanceof Pipe )
    141      return formatTrace( (Pipe) flowElement, message );
    142
    143    if( flowElement instanceof Tap )
    144      return formatTrace( (Tap) flowElement, message );
    145
    146    throw new UnsupportedOperationException( "cannot format type: " + flowElement.getClass().getName() );
    147    }
    148
    149  /**
    150   * Method formatRawTrace does not include the pipe name
    151   *
    152   * @param pipe    of type Pipe
    153   * @param message of type String
    154   * @return String
    155   */
    156  public static String formatRawTrace( Pipe pipe, String message )
    157    {
    158    return formatTrace( pipe, message, new TraceFormatter()
    159      {
    160      @Override
    161      public String format( String trace )
    162        {
    163        return "[" + trace + "]";
    164        }
    165      } );
    166    }
    167
    168  public static String formatTrace( final Pipe pipe, String message )
    169    {
    170    return formatTrace( pipe, message, new TraceFormatter()
    171      {
    172      @Override
    173      public String format( String trace )
    174        {
    175        return "[" + Util.truncate( pipe.getName(), 25 ) + "][" + trace + "]";
    176        }
    177      } );
    178    }
    179
    180  public static String formatTrace( final Tap tap, String message )
    181    {
    182    return formatTrace( tap, message, new TraceFormatter()
    183      {
    184      @Override
    185      public String format( String trace )
    186        {
    187        return "[" + Util.truncate( tap.toString(), 25 ) + "][" + trace + "]";
    188        }
    189      } );
    190    }
    191
    192  public static String formatTrace( Operation operation, String message )
    193    {
    194    if( !( operation instanceof BaseOperation ) )
    195      return message;
    196
    197    String trace = ( (BaseOperation) operation ).getTrace();
    198
    199    if( trace == null )
    200      return message;
    201
    202    return "[" + trace + "] " + message;
    203    }
    204
    205  public static String captureDebugTrace( Object target )
    206    {
    207    StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
    208
    209    StackTraceElement candidateUserCodeElement = null;
    210    StackTraceElement apiCallElement = null;
    211    Class<?> tracingBoundary = target.getClass();
    212    String boundaryClassName = tracingBoundary.getName();
    213
    214    // walk from the bottom of the stack(which is at the end of the array) upwards towards any boundary.
    215    // The apiCall is the element at the boundary and the previous stack element is the user code
    216    for( int i = stackTrace.length - 1; i >= 0; i-- )
    217      {
    218      StackTraceElement stackTraceElement = stackTrace[ i ];
    219      String stackClassName = stackTraceElement.getClassName();
    220
    221      boolean atApiBoundary = atApiBoundary( stackTraceElement.toString() );
    222
    223      if( ( stackClassName != null && ( stackClassName.startsWith( boundaryClassName ) ) || atApiBoundary ) )
    224        {
    225        // only record the apiCallElement if we're at an apiBoundary. That means
    226        // Trace will only have "apiMethod() @ call_location" when a registered
    227        // api boundary is found. The default for Cascading will just have
    228        // call_location.
    229        if( atApiBoundary )
    230          apiCallElement = stackTraceElement;
    231
    232        break;
    233        }
    234
    235      candidateUserCodeElement = stackTraceElement;
    236      }
    237
    238    String userCode = candidateUserCodeElement == null ? "" : candidateUserCodeElement.toString();
    239    String apiCall = "";
    240
    241    if( apiCallElement != null )
    242      {
    243      String method = apiCallElement.getMethodName();
    244
    245      if( method.equals( "<init>" ) )
    246        apiCall = String.format( "new %s()", getSimpleClassName( apiCallElement.getClassName() ) );
    247      else
    248        apiCall = String.format( "%s()", method );
    249      }
    250
    251    return userCode.isEmpty() ? apiCall : apiCall.isEmpty() ? userCode : String.format( "%s @ %s", apiCall, userCode );
    252    }
    253
    254  private static Object getSimpleClassName( String className )
    255    {
    256    if( className == null || className.isEmpty() )
    257      return "";
    258
    259    String parts[] = className.split( "\\." );
    260
    261    if( parts.length == 0 )
    262      return "";
    263
    264    return parts[ parts.length - 1 ];
    265    }
    266
    267  private static boolean atApiBoundary( String stackTraceElement )
    268    {
    269    for( Pattern boundary : registeredApiBoundaries.values() )
    270      {
    271      if( boundary.matcher( stackTraceElement ).matches() )
    272        return true;
    273      }
    274
    275    return false;
    276    }
    277
    278  public static String stringifyStackTrace( Throwable throwable, String lineSeparator, boolean trimLines, int lineLimit )
    279    {
    280    if( lineLimit == 0 )
    281      return null;
    282
    283    Writer traceWriter = new StringWriter();
    284    PrintWriter printWriter = new PrintWriter( traceWriter );
    285
    286    throwable.printStackTrace( printWriter );
    287
    288    String trace = traceWriter.toString();
    289
    290    if( lineSeparator.equals( System.getProperty( "line.separator" ) ) && !trimLines && lineLimit == -1 )
    291      return trace;
    292
    293    lineLimit = lineLimit == -1 ? Integer.MAX_VALUE : lineLimit;
    294
    295    StringBuilder buffer = new StringBuilder();
    296    LineNumberReader reader = new LineNumberReader( new StringReader( trace ) );
    297
    298    try
    299      {
    300      String line = reader.readLine();
    301
    302      while( line != null && reader.getLineNumber() - 1 < lineLimit )
    303        {
    304        if( reader.getLineNumber() > 1 )
    305          buffer.append( lineSeparator );
    306
    307        if( trimLines )
    308          line = line.trim();
    309
    310        buffer.append( line );
    311
    312        line = reader.readLine();
    313        }
    314      }
    315    catch( IOException exception )
    316      {
    317      // ignore - reading a string
    318      }
    319
    320    return buffer.toString();
    321    }
    322  }