View Javadoc
1   /*
2    * Copyright 2018 SPF4J.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * 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,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.spf4j.log;
17  
18  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
19  import java.io.BufferedWriter;
20  import java.io.IOException;
21  import java.io.OutputStream;
22  import java.io.OutputStreamWriter;
23  import java.io.PrintStream;
24  import java.io.UncheckedIOException;
25  import java.io.Writer;
26  import java.nio.charset.Charset;
27  import java.time.Instant;
28  import java.time.format.DateTimeFormatter;
29  import java.util.Arrays;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.concurrent.ConcurrentHashMap;
34  import java.util.concurrent.ConcurrentMap;
35  import javax.annotation.Nullable;
36  import javax.annotation.ParametersAreNonnullByDefault;
37  import javax.annotation.concurrent.ThreadSafe;
38  import org.slf4j.Marker;
39  import org.spf4j.base.CoreTextMediaType;
40  import org.spf4j.base.EscapeJsonStringAppendableWrapper;
41  import org.spf4j.base.Slf4jMessageFormatter;
42  import org.spf4j.base.Throwables;
43  import org.spf4j.base.avro.AThrowables;
44  import org.spf4j.base.avro.LogRecord;
45  import org.spf4j.io.ByteArrayBuilder;
46  import org.spf4j.io.ConfigurableAppenderSupplier;
47  import org.spf4j.io.ObjectAppender;
48  import org.spf4j.recyclable.impl.ArraySuppliers;
49  import org.spf4j.recyclable.impl.ThreadLocalRecyclingSupplier;
50  
51  /**
52   * A log printer. The format is not configurable, and this is intentional.
53   * create One instance of this printer and re-use it.
54   * @author Zoltan Farkas
55   */
56  @ParametersAreNonnullByDefault
57  @ThreadSafe
58  public final class LogPrinter {
59  
60    private static final ConcurrentMap<Charset, ThreadLocalRecyclingSupplier<Buffer>>
61            BUFFERS = new ConcurrentHashMap<>();
62  
63    private final ThreadLocalRecyclingSupplier<Buffer> tlBuffer;
64  
65    private final ConfigurableAppenderSupplier toStringer;
66  
67    private final DateTimeFormatter fmt;
68  
69    interface BufferedAppendable {
70  
71      Appendable getAppendable();
72  
73      Appendable getJsonStringEscapingAppendable();
74  
75      int getCurrentPos();
76  
77      void resetPos(int pos);
78  
79      static BufferedAppendable from(final StringBuilder sb) {
80  
81        return new BufferedAppendable() {
82  
83          private Appendable escaper = null;
84  
85          @Override
86          public Appendable getAppendable() {
87            return sb;
88          }
89  
90          @Override
91          public Appendable getJsonStringEscapingAppendable() {
92            if (escaper == null) {
93              escaper = new EscapeJsonStringAppendableWrapper(sb);
94            }
95            return escaper;
96          }
97  
98          @Override
99          public int getCurrentPos() {
100           return sb.length();
101         }
102 
103         @Override
104         public void resetPos(final int pos) {
105           sb.setLength(pos);
106         }
107       };
108     }
109 
110   }
111 
112   private static final class Buffer implements BufferedAppendable {
113 
114     private static final int MAX_BUFFER_SIZE = Integer.getInteger("spf4j.logPrinter", 1024 * 32);
115 
116     private final ByteArrayBuilder bab;
117 
118     private final Writer writer;
119 
120     private final EscapeJsonStringAppendableWrapper writerEscaper;
121 
122     Buffer(final Charset charset) {
123       bab = new ByteArrayBuilder(512, ArraySuppliers.Bytes.JAVA_NEW);
124       writer = new BufferedWriter(new OutputStreamWriter(bab, charset));
125       writerEscaper = new EscapeJsonStringAppendableWrapper(writer);
126     }
127 
128     private void clear() {
129       try {
130         writer.flush();
131       } catch (IOException ex) {
132         throw new RuntimeException(ex);
133       }
134       bab.reset();
135     }
136 
137     public Appendable getAppendable() {
138       return writer;
139     }
140 
141     public Appendable getJsonStringEscapingAppendable() {
142       return writerEscaper;
143     }
144 
145     private void flush() {
146       try {
147         writer.flush();
148       } catch (IOException ex) {
149         throw new UncheckedIOException(ex);
150       }
151     }
152 
153     private byte[] getBytes() {
154       return bab.getBuffer();
155     }
156 
157     private int size() {
158       return bab.size();
159     }
160 
161     @Override
162     public int getCurrentPos() {
163       flush();
164       return bab.size();
165     }
166 
167     @Override
168     @SuppressFBWarnings("EXS_EXCEPTION_SOFTENING_NO_CHECKED") //on purpose.
169     public void resetPos(final int pos) {
170       flush();
171       bab.resetCountTo(pos);
172     }
173 
174   }
175 
176   @SuppressFBWarnings("EI_EXPOSE_REP")
177   public ConfigurableAppenderSupplier getAppenderSupplier() {
178     return toStringer;
179   }
180 
181   public LogPrinter() {
182     this(DateTimeFormatter.ISO_INSTANT, Charset.defaultCharset());
183   }
184 
185   public LogPrinter(final Charset charset) {
186     this(DateTimeFormatter.ISO_INSTANT, charset);
187   }
188 
189   public LogPrinter(final DateTimeFormatter fmt, final Charset charset) {
190     this.fmt = fmt;
191     this.toStringer = new ConfigurableAppenderSupplier();
192     tlBuffer =  BUFFERS.computeIfAbsent(charset,
193             (cs) -> new ThreadLocalRecyclingSupplier<Buffer>(() -> new Buffer(cs)));
194   }
195 
196 
197   public OutputStream print(final Slf4jLogRecord record, final OutputStream os, final OutputStream errStream) {
198     if (record.getLevel() == Level.ERROR) {
199       print(record, errStream);
200       return errStream;
201     } else {
202       print(record, os);
203       return os;
204     }
205   }
206 
207   public void print(final Slf4jLogRecord record, final OutputStream os) {
208     Buffer buff = tlBuffer.get();
209     boolean recycle = true;
210     try {
211       buff.clear();
212       print(record, buff, "");
213       buff.flush();
214       int len = buff.size();
215       os.write(buff.getBytes(), 0, len);
216       if (len > Buffer.MAX_BUFFER_SIZE) {
217         recycle = false;
218       }
219     } catch (IOException ex) {
220       throw new UncheckedIOException(ex);
221     } finally {
222       if (recycle) {
223         tlBuffer.recycle(buff);
224       }
225     }
226   }
227 
228   public byte[] printToBytes(final Slf4jLogRecord record) {
229     Buffer buff = tlBuffer.get();
230     boolean recycle = true;
231     try {
232       buff.clear();
233       print(record, buff, "");
234       buff.flush();
235       int size = buff.size();
236       if (size > Buffer.MAX_BUFFER_SIZE) {
237         recycle = false;
238       }
239       return Arrays.copyOf(buff.getBytes(), size);
240     } catch (IOException ex) {
241       throw new UncheckedIOException(ex);
242     } finally {
243       if (recycle) {
244         tlBuffer.recycle(buff);
245       }
246     }
247   }
248 
249   public void print(final LogRecord record, final OutputStream os) throws IOException {
250     printTo(os, record, "");
251     os.flush();
252   }
253 
254   public void printTo(final StringBuilder sb, final Slf4jLogRecord record, final String annotate) {
255     try {
256       print(record, BufferedAppendable.from(sb), annotate);
257     } catch (IOException ex) {
258       throw new UncheckedIOException(ex);
259     }
260   }
261 
262   public void printTo(final OutputStream stream, final LogRecord record, final String annotate) {
263     Buffer buff = tlBuffer.get();
264     buff.clear();
265     try {
266       print(record, buff, annotate);
267       buff.flush();
268       stream.write(buff.getBytes(), 0, buff.size());
269       stream.flush();
270     } catch (IOException ex) {
271       throw new UncheckedIOException(ex);
272     }
273   }
274 
275   public void printTo(final PrintStream stream, final Slf4jLogRecord record, final String annotate) {
276     Buffer buff = tlBuffer.get();
277     buff.clear();
278     try {
279       print(record, buff, annotate);
280       buff.flush();
281       stream.write(buff.getBytes(), 0, buff.size());
282       stream.flush();
283     } catch (IOException ex) {
284       throw new UncheckedIOException(ex);
285     }
286   }
287 
288   static void printMarker(final Marker marker, final Appendable wr,
289           final Appendable wrapper)
290           throws IOException {
291       if (marker.hasReferences()) {
292         wr.append('{');
293         wr.append('"');
294         wrapper.append(marker.getName());
295         wr.append("\":[");
296         Iterator<Marker> it = marker.iterator();
297         if (it.hasNext()) {
298           printMarker(it.next(), wr, wrapper);
299           while (it.hasNext()) {
300             wr.append(',');
301             printMarker(it.next(), wr, wrapper);
302           }
303         }
304         wr.append("]}");
305       } else {
306         wr.append('"');
307         wrapper.append(marker.getName());
308         wr.append('"');
309       }
310    }
311 
312 
313    private void print(final Slf4jLogRecord record, final BufferedAppendable app, final String annotate)
314           throws IOException {
315     Appendable wr = app.getAppendable();
316     Appendable wrapper = app.getJsonStringEscapingAppendable();
317     wr.append(annotate);
318     fmt.formatTo(Instant.ofEpochMilli(record.getTimeStamp()), wr);
319     wr.append(' ');
320     String level = record.getLevel().toString();
321     wr.append(level);
322     wr.append(' ');
323     Marker marker = record.getMarker();
324     if (marker != null) {
325       printMarker(marker, wr, wrapper);
326       wr.append(' ');
327     }
328     Throwables.writeAbreviatedClassName(record.getLoggerName(), wr);
329     wr.append(" \"");
330     wrapper.append(record.getThreadName());
331     wr.append("\" \"");
332     Object[] arguments = record.getArguments();
333     int i = Slf4jMessageFormatter.format(LogPrinter::exHandle, 0, wrapper, record.getMessageFormat(),
334             toStringer, arguments);
335     wr.append("\" ");
336     Throwable t = null;
337     if (i < arguments.length) {
338       boolean first = true;
339       for (; i < arguments.length; i++) {
340         Object arg = arguments[i];
341         if (arg instanceof Throwable) {
342           if (t == null) {
343             t = (Throwable) arg;
344           } else {
345             t.addSuppressed((Throwable) arg); // not ideal
346           }
347         } else {
348           if (!first) {
349             wr.append(", ");
350           } else {
351             wr.append('[');
352             first = false;
353           }
354           printJsonObject(arg, app);
355         }
356       }
357       if (!first) {
358         wr.append(']');
359       }
360     }
361     if (t != null) {
362       wr.append('\n');
363       Throwables.writeTo(t, wr, Throwables.PackageDetail.SHORT);
364     } else {
365       wr.append('\n');
366     }
367   }
368 
369    private void print(final LogRecord record, final BufferedAppendable ba, final String annotate)
370           throws IOException {
371     Appendable wr = ba.getAppendable();
372     Appendable wrapper = ba.getJsonStringEscapingAppendable();
373     wr.append(annotate);
374     wr.append('"');
375     wrapper.append(record.getOrigin());
376     wr.append("\" ");
377     fmt.formatTo(record.getTs(), wr);
378     wr.append(' ');
379     String level = record.getLevel().toString();
380     wr.append(level);
381     wr.append(' ');
382     wr.append(record.getLogger());
383     wr.append(" \"");
384     wrapper.append(record.getThr());
385     wrapper.append(':');
386     wrapper.append(record.getTrId());
387     wr.append("\" \"");
388     Slf4jMessageFormatter.format(wrapper, record.getMsg(), record.getMsgArgs().toArray());
389     wr.append("\" ");
390     Map<String, Object> attrs = record.getAttrs();
391     List<Object> xtra = record.getXtra();
392     if (attrs.size() + xtra.size() > 0) {
393       boolean first = true;
394       wr.append('[');
395       for (Map.Entry<String, Object> entry : attrs.entrySet()) {
396         if (first) {
397           first = false;
398         } else {
399           wr.append(',');
400         }
401         printJsonObject(entry, ba);
402       }
403       for (Object obj : xtra) {
404         if (first) {
405           first = false;
406         } else {
407           wr.append(',');
408         }
409         printJsonObject(obj, ba);
410       }
411       wr.append(']');
412     }
413     org.spf4j.base.avro.Throwable t = record.getThrowable();
414     if (t != null) {
415       wr.append('\n');
416       AThrowables.writeTo(t, wr, Throwables.PackageDetail.SHORT, true, "");
417     } else {
418       wr.append('\n');
419     }
420   }
421 
422   /**
423    * Function that will write the Object as a json representation.
424    * If json appender not available a json string value will be written.
425    * @param obj
426    * @param wr
427    * @param wrapper
428    * @throws IOException
429    */
430   private void printJsonObject(@Nullable final Object obj,
431           final BufferedAppendable app) throws IOException {
432     if (obj == null) {
433       app.getAppendable().append("null");
434     } else {
435       ObjectAppender ostrApp = toStringer.get(CoreTextMediaType.APPLICATION_JSON, obj.getClass());
436       if (ostrApp != null) {
437         int currentPos = app.getCurrentPos();
438         try {
439           ostrApp.append(obj, app.getAppendable(), toStringer);
440           return;
441         } catch (IOException | RuntimeException e) {
442           app.resetPos(currentPos);
443         }
444       }
445       Appendable wr = app.getAppendable();
446       Appendable wrapper = app.getJsonStringEscapingAppendable();
447       ostrApp = toStringer.get(CoreTextMediaType.TEXT_PLAIN, obj.getClass());
448       wr.append('"');
449       int currentPos = app.getCurrentPos();
450       try {
451         ostrApp.append(obj, wrapper, toStringer);
452       }  catch (IOException | RuntimeException e) {
453         app.resetPos(currentPos);
454         exHandle(obj, wrapper, e);
455       }
456       wr.append('"');
457     }
458   }
459 
460   static void exHandle(final Object obj, final Appendable sbuf, final Throwable t) throws IOException {
461     String className = obj.getClass().getName();
462     sbuf.append("[FAILED toString() for ");
463     sbuf.append(className);
464     sbuf.append("]{");
465     Throwables.writeTo(t, sbuf, Throwables.PackageDetail.SHORT);
466     sbuf.append('}');
467   }
468 
469   @Override
470   public String toString() {
471     return "LogPrinter{" + "toStringer=" + toStringer + ", fmt=" + fmt + '}';
472   }
473 
474 }