View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. 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, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.logging;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.buffer.ByteBufHolder;
20  import io.netty.channel.ChannelHandler;
21  import io.netty.channel.ChannelHandler.Sharable;
22  import io.netty.channel.ChannelHandlerAdapter;
23  import io.netty.channel.ChannelHandlerContext;
24  import io.netty.channel.ChannelPromise;
25  import io.netty.util.internal.StringUtil;
26  import io.netty.util.internal.logging.InternalLogLevel;
27  import io.netty.util.internal.logging.InternalLogger;
28  import io.netty.util.internal.logging.InternalLoggerFactory;
29  
30  import java.net.SocketAddress;
31  
32  /**
33   * A {@link ChannelHandler} that logs all events using a logging framework.
34   * By default, all events are logged at <tt>DEBUG</tt> level.
35   */
36  @Sharable
37  @SuppressWarnings("StringBufferReplaceableByString")
38  public class LoggingHandler extends ChannelHandlerAdapter {
39  
40      private static final LogLevel DEFAULT_LEVEL = LogLevel.DEBUG;
41  
42      private static final String NEWLINE = StringUtil.NEWLINE;
43  
44      private static final String[] BYTE2HEX = new String[256];
45      private static final String[] HEXPADDING = new String[16];
46      private static final String[] BYTEPADDING = new String[16];
47      private static final char[] BYTE2CHAR = new char[256];
48      private static final String[] HEXDUMP_ROWPREFIXES = new String[65536 >>> 4];
49  
50      static {
51          int i;
52  
53          // Generate the lookup table for byte-to-hex-dump conversion
54          for (i = 0; i < BYTE2HEX.length; i ++) {
55              BYTE2HEX[i] = ' ' + StringUtil.byteToHexStringPadded(i);
56          }
57  
58          // Generate the lookup table for hex dump paddings
59          for (i = 0; i < HEXPADDING.length; i ++) {
60              int padding = HEXPADDING.length - i;
61              StringBuilder buf = new StringBuilder(padding * 3);
62              for (int j = 0; j < padding; j ++) {
63                  buf.append("   ");
64              }
65              HEXPADDING[i] = buf.toString();
66          }
67  
68          // Generate the lookup table for byte dump paddings
69          for (i = 0; i < BYTEPADDING.length; i ++) {
70              int padding = BYTEPADDING.length - i;
71              StringBuilder buf = new StringBuilder(padding);
72              for (int j = 0; j < padding; j ++) {
73                  buf.append(' ');
74              }
75              BYTEPADDING[i] = buf.toString();
76          }
77  
78          // Generate the lookup table for byte-to-char conversion
79          for (i = 0; i < BYTE2CHAR.length; i ++) {
80              if (i <= 0x1f || i >= 0x7f) {
81                  BYTE2CHAR[i] = '.';
82              } else {
83                  BYTE2CHAR[i] = (char) i;
84              }
85          }
86  
87          // Generate the lookup table for the start-offset header in each row (up to 64KiB).
88          for (i = 0; i < HEXDUMP_ROWPREFIXES.length; i ++) {
89              StringBuilder buf = new StringBuilder(12);
90              buf.append(NEWLINE);
91              buf.append(Long.toHexString(i << 4 & 0xFFFFFFFFL | 0x100000000L));
92              buf.setCharAt(buf.length() - 9, '|');
93              buf.append('|');
94              HEXDUMP_ROWPREFIXES[i] = buf.toString();
95          }
96      }
97  
98      protected final InternalLogger logger;
99      protected final InternalLogLevel internalLevel;
100     private final LogLevel level;
101 
102     /**
103      * Creates a new instance whose logger name is the fully qualified class
104      * name of the instance with hex dump enabled.
105      */
106     public LoggingHandler() {
107         this(DEFAULT_LEVEL);
108     }
109 
110     /**
111      * Creates a new instance whose logger name is the fully qualified class
112      * name of the instance.
113      *
114      * @param level the log level
115      */
116     public LoggingHandler(LogLevel level) {
117         if (level == null) {
118             throw new NullPointerException("level");
119         }
120 
121         logger = InternalLoggerFactory.getInstance(getClass());
122         this.level = level;
123         internalLevel = level.toInternalLevel();
124     }
125 
126     /**
127      * Creates a new instance with the specified logger name and with hex dump
128      * enabled.
129      *
130      * @param clazz the class type to generate the logger for
131      */
132     public LoggingHandler(Class<?> clazz) {
133         this(clazz, DEFAULT_LEVEL);
134     }
135 
136     /**
137      * Creates a new instance with the specified logger name.
138      *
139      * @param clazz the class type to generate the logger for
140      * @param level the log level
141      */
142     public LoggingHandler(Class<?> clazz, LogLevel level) {
143         if (clazz == null) {
144             throw new NullPointerException("clazz");
145         }
146         if (level == null) {
147             throw new NullPointerException("level");
148         }
149 
150         logger = InternalLoggerFactory.getInstance(clazz);
151         this.level = level;
152         internalLevel = level.toInternalLevel();
153     }
154 
155     /**
156      * Creates a new instance with the specified logger name using the default log level.
157      *
158      * @param name the name of the class to use for the logger
159      */
160     public LoggingHandler(String name) {
161         this(name, DEFAULT_LEVEL);
162     }
163 
164     /**
165      * Creates a new instance with the specified logger name.
166      *
167      * @param name the name of the class to use for the logger
168      * @param level the log level
169      */
170     public LoggingHandler(String name, LogLevel level) {
171         if (name == null) {
172             throw new NullPointerException("name");
173         }
174         if (level == null) {
175             throw new NullPointerException("level");
176         }
177 
178         logger = InternalLoggerFactory.getInstance(name);
179         this.level = level;
180         internalLevel = level.toInternalLevel();
181     }
182 
183     /**
184      * Returns the {@link LogLevel} that this handler uses to log
185      */
186     public LogLevel level() {
187         return level;
188     }
189 
190     @Override
191     public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
192         if (logger.isEnabled(internalLevel)) {
193             logger.log(internalLevel, format(ctx, "REGISTERED"));
194         }
195         ctx.fireChannelRegistered();
196     }
197 
198     @Override
199     public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
200         if (logger.isEnabled(internalLevel)) {
201             logger.log(internalLevel, format(ctx, "UNREGISTERED"));
202         }
203         ctx.fireChannelUnregistered();
204     }
205 
206     @Override
207     public void channelActive(ChannelHandlerContext ctx) throws Exception {
208         if (logger.isEnabled(internalLevel)) {
209             logger.log(internalLevel, format(ctx, "ACTIVE"));
210         }
211         ctx.fireChannelActive();
212     }
213 
214     @Override
215     public void channelInactive(ChannelHandlerContext ctx) throws Exception {
216         if (logger.isEnabled(internalLevel)) {
217             logger.log(internalLevel, format(ctx, "INACTIVE"));
218         }
219         ctx.fireChannelInactive();
220     }
221 
222     @Override
223     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
224         if (logger.isEnabled(internalLevel)) {
225             logger.log(internalLevel, format(ctx, "EXCEPTION", cause), cause);
226         }
227         ctx.fireExceptionCaught(cause);
228     }
229 
230     @Override
231     public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
232         if (logger.isEnabled(internalLevel)) {
233             logger.log(internalLevel, format(ctx, "USER_EVENT", evt));
234         }
235         ctx.fireUserEventTriggered(evt);
236     }
237 
238     @Override
239     public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception {
240         if (logger.isEnabled(internalLevel)) {
241             logger.log(internalLevel, format(ctx, "BIND", localAddress));
242         }
243         ctx.bind(localAddress, promise);
244     }
245 
246     @Override
247     public void connect(
248             ChannelHandlerContext ctx,
249             SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) throws Exception {
250         if (logger.isEnabled(internalLevel)) {
251             logger.log(internalLevel, format(ctx, "CONNECT", remoteAddress, localAddress));
252         }
253         ctx.connect(remoteAddress, localAddress, promise);
254     }
255 
256     @Override
257     public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
258         if (logger.isEnabled(internalLevel)) {
259             logger.log(internalLevel, format(ctx, "DISCONNECT"));
260         }
261         ctx.disconnect(promise);
262     }
263 
264     @Override
265     public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
266         if (logger.isEnabled(internalLevel)) {
267             logger.log(internalLevel, format(ctx, "CLOSE"));
268         }
269         ctx.close(promise);
270     }
271 
272     @Override
273     public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
274         if (logger.isEnabled(internalLevel)) {
275             logger.log(internalLevel, format(ctx, "DEREGISTER"));
276         }
277         ctx.deregister(promise);
278     }
279 
280     @Override
281     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
282         if (logger.isEnabled(internalLevel)) {
283             logger.log(internalLevel, format(ctx, "RECEIVED", msg));
284         }
285         ctx.fireChannelRead(msg);
286     }
287 
288     @Override
289     public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
290         if (logger.isEnabled(internalLevel)) {
291             logger.log(internalLevel, format(ctx, "WRITE", msg));
292         }
293         ctx.write(msg, promise);
294     }
295 
296     @Override
297     public void flush(ChannelHandlerContext ctx) throws Exception {
298         if (logger.isEnabled(internalLevel)) {
299             logger.log(internalLevel, format(ctx, "FLUSH"));
300         }
301         ctx.flush();
302     }
303 
304     /**
305      * Formats an event and returns the formatted message.
306      *
307      * @param eventName the name of the event
308      */
309     protected String format(ChannelHandlerContext ctx, String eventName) {
310         String chStr = ctx.channel().toString();
311         return new StringBuilder(chStr.length() + 1 + eventName.length())
312             .append(chStr)
313             .append(' ')
314             .append(eventName)
315             .toString();
316     }
317 
318     /**
319      * Formats an event and returns the formatted message.
320      *
321      * @param eventName the name of the event
322      * @param arg       the argument of the event
323      */
324     protected String format(ChannelHandlerContext ctx, String eventName, Object arg) {
325         if (arg instanceof ByteBuf) {
326             return formatByteBuf(ctx, eventName, (ByteBuf) arg);
327         } else if (arg instanceof ByteBufHolder) {
328             return formatByteBufHolder(ctx, eventName, (ByteBufHolder) arg);
329         } else {
330             return formatSimple(ctx, eventName, arg);
331         }
332     }
333 
334     /**
335      * Formats an event and returns the formatted message.  This method is currently only used for formatting
336      * {@link ChannelHandler#connect(ChannelHandlerContext, SocketAddress, SocketAddress, ChannelPromise)}.
337      *
338      * @param eventName the name of the event
339      * @param firstArg  the first argument of the event
340      * @param secondArg the second argument of the event
341      */
342     protected String format(ChannelHandlerContext ctx, String eventName, Object firstArg, Object secondArg) {
343         if (secondArg == null) {
344             return formatSimple(ctx, eventName, firstArg);
345         }
346 
347         String chStr = ctx.channel().toString();
348         String arg1Str = String.valueOf(firstArg);
349         String arg2Str = secondArg.toString();
350         StringBuilder buf = new StringBuilder(
351                 chStr.length() + 1 + eventName + 2 + arg1Str.length() + 2 + arg2Str.length());
352         buf.append(chStr).append(' ').append(eventName).append(": ").append(arg1Str).append(", ").append(arg2Str);
353         return buf.toString();
354     }
355 
356     /**
357      * Generates the default log message of the specified event whose argument is a {@link ByteBuf}.
358      */
359     private static String formatByteBuf(ChannelHandlerContext ctx, String eventName, ByteBuf msg) {
360         String chStr = ctx.channel().toString();
361         int length = msg.readableBytes();
362         if (length == 0) {
363             StringBuilder buf = new StringBuilder(chStr.length() + 1 + eventName.length() + 4);
364             buf.append(chStr).append(' ').append(eventName).append(": 0B");
365             return buf.toString();
366         } else {
367             int rows = length / 16 + (length % 15 == 0? 0 : 1) + 4;
368             StringBuilder buf = new StringBuilder(chStr.length() + 1 + eventName.length() + 2 + 10 + 1 + 2 + rows * 80);
369 
370             buf.append(chStr).append(' ').append(eventName).append(": ").append(length).append('B');
371             appendHexDump(buf, msg);
372 
373             return buf.toString();
374         }
375     }
376 
377     /**
378      * Generates the default log message of the specified event whose argument is a {@link ByteBufHolder}.
379      */
380     private static String formatByteBufHolder(ChannelHandlerContext ctx, String eventName, ByteBufHolder msg) {
381         String chStr = ctx.channel().toString();
382         String msgStr = msg.toString();
383         ByteBuf content = msg.content();
384         int length = content.readableBytes();
385         if (length == 0) {
386             StringBuilder buf = new StringBuilder(chStr.length() + 1 + eventName.length() + 2 + msgStr.length() + 4);
387             buf.append(chStr).append(' ').append(eventName).append(", ").append(msgStr).append(", 0B");
388             return buf.toString();
389         } else {
390             int rows = length / 16 + (length % 15 == 0? 0 : 1) + 4;
391             StringBuilder buf = new StringBuilder(
392                     chStr.length() + 1 + eventName.length() + 2 + msgStr.length() + 2 + 10 + 1 + 2 + rows * 80);
393 
394             buf.append(chStr).append(' ').append(eventName).append(": ")
395                .append(msgStr).append(", ").append(length).append('B');
396             appendHexDump(buf, content);
397 
398             return buf.toString();
399         }
400     }
401 
402     /**
403      * Appends the prettifies multi-line hexadecimal dump of the specified {@link ByteBuf} to the specified
404      * {@link StringBuilder}.
405      */
406     protected static void appendHexDump(StringBuilder dump, ByteBuf buf) {
407         dump.append(
408                 NEWLINE + "         +-------------------------------------------------+" +
409                 NEWLINE + "         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |" +
410                 NEWLINE + "+--------+-------------------------------------------------+----------------+");
411 
412         final int startIndex = buf.readerIndex();
413         final int endIndex = buf.writerIndex();
414         final int length = endIndex - startIndex;
415         final int fullRows = length >>> 4;
416         final int remainder = length & 0xF;
417 
418         // Dump the rows which have 16 bytes.
419         for (int row = 0; row < fullRows; row ++) {
420             int rowStartIndex = row << 4;
421 
422             // Per-row prefix.
423             appendHexDumpRowPrefix(dump, row, rowStartIndex);
424 
425             // Hex dump
426             int rowEndIndex = rowStartIndex + 16;
427             for (int j = rowStartIndex; j < rowEndIndex; j ++) {
428                 dump.append(BYTE2HEX[buf.getUnsignedByte(j)]);
429             }
430             dump.append(" |");
431 
432             // ASCII dump
433             for (int j = rowStartIndex; j < rowEndIndex; j ++) {
434                 dump.append(BYTE2CHAR[buf.getUnsignedByte(j)]);
435             }
436             dump.append('|');
437         }
438 
439         // Dump the last row which has less than 16 bytes.
440         if (remainder != 0) {
441             int rowStartIndex = fullRows << 4;
442             appendHexDumpRowPrefix(dump, fullRows, rowStartIndex);
443 
444             // Hex dump
445             int rowEndIndex = rowStartIndex + remainder;
446             for (int j = rowStartIndex; j < rowEndIndex; j ++) {
447                 dump.append(BYTE2HEX[buf.getUnsignedByte(j)]);
448             }
449             dump.append(HEXPADDING[remainder]);
450             dump.append(" |");
451 
452             // Ascii dump
453             for (int j = rowStartIndex; j < rowEndIndex; j ++) {
454                 dump.append(BYTE2CHAR[buf.getUnsignedByte(j)]);
455             }
456             dump.append(BYTEPADDING[remainder]);
457             dump.append('|');
458         }
459 
460         dump.append(NEWLINE + "+--------+-------------------------------------------------+----------------+");
461     }
462 
463     /**
464      * Appends the prefix of each hex dump row.  Uses the look-up table for the buffer <= 64 KiB.
465      */
466     private static void appendHexDumpRowPrefix(StringBuilder dump, int row, int rowStartIndex) {
467         if (row < HEXDUMP_ROWPREFIXES.length) {
468             dump.append(HEXDUMP_ROWPREFIXES[row]);
469         } else {
470             dump.append(NEWLINE);
471             dump.append(Long.toHexString(rowStartIndex & 0xFFFFFFFFL | 0x100000000L));
472             dump.setCharAt(dump.length() - 9, '|');
473             dump.append('|');
474         }
475     }
476 
477     /**
478      * Generates the default log message of the specified event whose argument is an arbitrary object.
479      */
480     private static String formatSimple(ChannelHandlerContext ctx, String eventName, Object msg) {
481         String chStr = ctx.channel().toString();
482         String msgStr = String.valueOf(msg);
483         StringBuilder buf = new StringBuilder(chStr.length() + 1 + eventName.length() + 2 + msgStr.length());
484         return buf.append(chStr).append(' ').append(eventName).append(": ").append(msgStr).toString();
485     }
486 }