1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
53
54
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")
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);
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
424
425
426
427
428
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 }