aboutsummaryrefslogtreecommitdiffstats
path: root/reference/logging-slf4j/src/main/java/org/onap/logging/ref/slf4j/ONAPLogAdapter.java
blob: d26cd130383352f84d212137104780ef1ed57769 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
/**
 * ============LICENSE_START=======================================================
 * org.onap.logging
 * ================================================================================
 * Copyright © 2018 Amdocs
 * All rights reserved.
 * ================================================================================
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * ============LICENSE_END=========================================================
 */

package org.onap.logging.ref.slf4j;

import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.MDC;
import org.slf4j.Marker;
import org.slf4j.event.Level;

/**
 * Extensible adapter for cheaply meeting ONAP logging obligations using
 * an SLF4J facade.
 *
 * <p>This can be used with any SLF4J-compatible logging provider, with
 * appropriate provider configuration.</p>
 *
 * <p>The basics are that:
 * <ul>
 *     <li>{@link #entering} sets all MDCs.</li>
 *     <li>{@link #exiting} unsets all MDCs *and* logs response information.</li>
 *     <li>{@link #invoke} logs and returns a UUID to passed during invocation,
 *     and optionally sets these for you on your downstream request by way of
 *     an adapter.</li>
 *     <li>Call {@link #getServiceDescriptor()} and its setters to set service-related MDCs.</li>
 *     <li>Call {@link #getResponseDescriptor()} and its setters to set response-related MDCs.</li>
 * </ul>
 * </p>
 *
 * <p>Minimal usage is:
 * <ol>
 *     <li>#entering(RequestAdapter)</li>
 *     <li>#invoke, #invoke, ...</li>
 *     <li>#getResponse + setters (or #setResponse)</li>
 *     <li>#exiting</li>
 * </ol>
 * </p>
 *
 * <p> ... if you're happy for service information to be automatically derived as follows:
 * <ul>
 *     <li><tt>ServiceName</tt> - from <tt>HttpServletRequest#getRequestURI()</tt></li>
 *     <li><tt>InstanceUUID</tt> - classloader-scope UUID.</li>
 * </ul>
 * </p>
 *
 * <p>... and if those defaults don't suit, then you can override using properties on
 * {@link #getServiceDescriptor()}, or by injecting your own adapter using
 * {@link #setServiceDescriptor(ServiceDescriptor)}, or by overriding
 * a <tt>protected</tt> methods like{@link #setEnteringMDCs}.</p>
 *
 * <p>For everything else:
 * <ul>
 *     <li>The underlying SLF4J {@link Logger} can be retrieved using {@link #unwrap}.
 *     Use this or create your own using the usual SLF4J factor.</li>
 *     <li>Set whatever MDCs you like.</li>
 *     <li>Log whatever else you like.</li>
 * </ul>
 * </p>
 */
public class ONAPLogAdapter {

    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    //
    // Constants.
    //
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    /** String constant for messages <tt>ENTERING</tt>, <tt>EXITING</tt>, etc. */
    private static final String EMPTY_MESSAGE = "";

    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    //
    // Fields.
    //
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    /** Automatic UUID, overrideable per adapter or per invocation. */
    private static UUID sInstanceUUID = UUID.randomUUID();

    /** Logger delegate. */
    private Logger mLogger;

    /** Overrideable descriptor for the service doing the logging. */
    private ServiceDescriptor mServiceDescriptor = new ServiceDescriptor();

    /** Overrideable descriptor for the response returned by the service doing the logging. */
    private ResponseDescriptor mResponseDescriptor = new ResponseDescriptor();

    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    //
    // Constructors.
    //
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    /**
     * Construct adapter.
     *
     * @param logger non-null logger.
     */
    public ONAPLogAdapter(final Logger logger) {
        this.mLogger = checkNotNull(logger);
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    //
    // Public methods.
    //
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    /**
     * Get logger.
     *
     * @return unwrapped logger.
     */
    public Logger unwrap() {
        return this.mLogger;
    }

    /**
     * Report <tt>ENTERING</tt> marker.
     *
     * @param request non-null incoming request (wrapper).
     * @return this.
     */
    public ONAPLogAdapter entering(final RequestAdapter request) {

        checkNotNull(request);

        // Default the service name.

        this.setEnteringMDCs(request);
        this.mLogger.info(ONAPLogConstants.Markers.ENTRY, EMPTY_MESSAGE);

        return this;
    }

    /**
     * Report <tt>ENTERING</tt> marker.
     *
     * @param request non-null incoming request.
     * @return this.
     */
    public ONAPLogAdapter entering(final HttpServletRequest request) {
        return this.entering(new HttpServletRequestAdapter(checkNotNull(request)));
    }

    /**
     * Report <tt>EXITING</tt> marker.
     *
     * @return this.
     */
    public ONAPLogAdapter exiting() {
        try {
            this.mResponseDescriptor.setMDCs();
            this.mLogger.info(ONAPLogConstants.Markers.EXIT, EMPTY_MESSAGE);
        }
        finally {
            MDC.clear();
        }
        return this;
    }

    /**
     * Report pending invocation with <tt>INVOKE</tt> marker.
     *
     * <p>If you call this variant, then YOU are assuming responsibility for
     * setting the requisite ONAP headers.</p>
     *
     * @param sync whether synchronous.
     * @return invocation ID to be passed with invocation.
     */
    public UUID invoke(final ONAPLogConstants.InvocationMode sync) {

        final UUID invocationID = UUID.randomUUID();

        // Derive SYNC/ASYNC marker.

        final Marker marker = (sync == null) ? ONAPLogConstants.Markers.INVOKE : sync.getMarker();

        // Log INVOKE*, with the invocationID as the message body.
        // (We didn't really want this kind of behavior in the standard,
        // but is it worse than new, single-message MDC?)

        this.mLogger.info(marker, "{}", invocationID);
        return invocationID;
    }

    /**
     * Report pending invocation with <tt>INVOKE</tt> marker,
     * setting standard ONAP logging headers automatically.
     *
     * @param builder request builder, for setting headers.
     * @param sync whether synchronous, nullable.
     * @return invocation ID to be passed with invocation.
     */
    public UUID invoke(final RequestBuilder builder,
                       final ONAPLogConstants.InvocationMode sync) {

        // Sync can be defaulted. Builder cannot.

        checkNotNull(builder);

        // Log INVOKE, and retain invocation ID for header + return.

        final UUID invocationID = this.invoke(sync);

        // Set standard HTTP headers on (southbound request) builder.

        builder.setHeader(ONAPLogConstants.Headers.REQUEST_ID,
                defaultToEmpty(MDC.get(ONAPLogConstants.MDCs.REQUEST_ID)));
        builder.setHeader(ONAPLogConstants.Headers.INVOCATION_ID,
                defaultToEmpty(invocationID));
        builder.setHeader(ONAPLogConstants.Headers.PARTNER_NAME,
                defaultToEmpty(MDC.get(ONAPLogConstants.MDCs.PARTNER_NAME)));

        return invocationID;
    }

    /**
     * Report vanilla <tt>INVOKE</tt> marker.
     *
     * @param builder builder for downstream requests, if you want the
     *                standard ONAP headers to be added automatically.
     * @return invocation ID to be passed with invocation.
     */
    public UUID invoke(final RequestBuilder builder) {
        return this.invoke(builder, (ONAPLogConstants.InvocationMode)null);
    }

    /**
     * Get descriptor, for overriding service details.
     * @return non-null descriptor.
     */
    public ServiceDescriptor getServiceDescriptor() {
        return checkNotNull(this.mServiceDescriptor);
    }

    /**
     * Override {@link ServiceDescriptor}.
     * @param d non-null override.
     * @return this.
     */
    public ONAPLogAdapter setServiceDescriptor(final ServiceDescriptor d) {
        this.mServiceDescriptor = checkNotNull(d);
        return this;
    }

    /**
     * Get descriptor, for setting response details.
     * @return non-null descriptor.
     */
    public ResponseDescriptor getResponseDescriptor() {
        return checkNotNull(this.mResponseDescriptor);
    }

    /**
     * Override {@link ResponseDescriptor}.
     * @param d non-null override.
     * @return this.
     */
    public ONAPLogAdapter setResponseDescriptor(final ResponseDescriptor d) {
        this.mResponseDescriptor = checkNotNull(d);
        return this;
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    //
    // Protected methods.
    //
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    /**
     * Set MDCs that persist for the duration of an invocation.
     *
     * <p>It would be better to roll this into {@link #entering}, like
     * with {@link #exiting}. Then it would be easier to do, but it
     * would mean more work. </p>
     *
     * @param request incoming HTTP request.
     * @return this.
     */
    protected ONAPLogAdapter setEnteringMDCs(final RequestAdapter<?> request) {

        // Extract MDC values from standard HTTP headers.

        final String requestID = defaultToUUID(request.getHeader(ONAPLogConstants.Headers.REQUEST_ID));
        final String invocationID = defaultToUUID(request.getHeader(ONAPLogConstants.Headers.INVOCATION_ID));
        final String partnerName = defaultToEmpty(request.getHeader(ONAPLogConstants.Headers.PARTNER_NAME));

        // Set standard MDCs. Override this entire method if you want to set
        // others, OR set them BEFORE or AFTER the invocation of #entering,
        // depending on where you need them to appear, OR extend the
        // ServiceDescriptor to add them.

        MDC.put(ONAPLogConstants.MDCs.INVOKE_TIMESTAMP,
                ZonedDateTime.now(ZoneOffset.UTC)
                        .format(DateTimeFormatter.ISO_INSTANT));
        MDC.put(ONAPLogConstants.MDCs.REQUEST_ID, requestID);
        MDC.put(ONAPLogConstants.MDCs.INVOCATION_ID, invocationID);
        MDC.put(ONAPLogConstants.MDCs.PARTNER_NAME, partnerName);
        MDC.put(ONAPLogConstants.MDCs.CLIENT_IP_ADDRESS, defaultToEmpty(request.getClientAddress()));
        MDC.put(ONAPLogConstants.MDCs.SERVER_FQDN, defaultToEmpty(request.getServerAddress()));

        // Delegate to the service adapter, for service-related DMCs.

        this.mServiceDescriptor.setMDCs();

        // Default the service name to the requestURI, in the event that
        // no value has been provided.

        if (MDC.get(ONAPLogConstants.MDCs.SERVICE_NAME) == null) {
            MDC.put(ONAPLogConstants.MDCs.SERVICE_NAME, request.getRequestURI());
        }

        return this;
    }

    /**
     * Dependency-free nullcheck.
     *
     * @param in to be checked.
     * @param <T> argument (and return) type.
     * @return input arg.
     */
    protected static <T> T checkNotNull(final T in) {
        if (in == null) {
            throw new NullPointerException();
        }
        return in;
    }

    /**
     * Dependency-free string default.
     *
     * @param in to be filtered.
     * @return input string or null.
     */
    protected static String defaultToEmpty(final Object in) {
        if (in == null) {
            return "";
        }
        return in.toString();
    }

    /**
     * Dependency-free string default.
     *
     * @param in to be filtered.
     * @return input string or null.
     */
    protected static String defaultToUUID(final String in) {
        if (in == null) {
            return UUID.randomUUID().toString();
        }
        return in;
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    //
    // Inner classes.
    //
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    /**
     * Extensible descriptor for reporting service details.
     *
     * <p>In most cases extension isn't required. </p>
     */
    public static class ServiceDescriptor {

        /** <tt>ServiceName</tt>. */
        protected String mName;

        /** <tt>InstanceUUID</tt>. */
        protected String mUUID = sInstanceUUID.toString();

        /**
         * Set name.
         * @param name <tt>ServiceName</tt>.
         * @return this.
         */
        public ServiceDescriptor setServiceName(final String name) {
            this.mName = name;
            return this;
        }

        /**
         * Set name.
         * @param uuid <tt>InstanceUUID</tt>.
         * @return this.
         */
        public ServiceDescriptor setServiceUUID(final String uuid) {
            this.mUUID = uuid;
            return this;
        }

        /**
         * Set MDCs. Once set they remain set until everything is cleared.
         */
        protected void setMDCs() {
            MDC.put(ONAPLogConstants.MDCs.SERVICE_NAME, defaultToEmpty(this.mName));
            MDC.put(ONAPLogConstants.MDCs.INSTANCE_UUID, defaultToEmpty(this.mUUID));
        }
    }

    /**
     * Response is different in that response MDCs are normally only
     * reported once, for a single log message. (But there's no method
     * for clearing them, because this is only expected to be called
     * during <tt>#exiting</tt>.)
     */
    public static class ResponseDescriptor {

        /** Response errorcode. */
        protected String mCode;

        /** Response description. */
        protected String mDescription;

        /** Response severity. */
        protected Level mSeverity;

        /** Response status, of {<tt>COMPLETED</tt>, <tt>ERROR</tt>}. */
        protected ONAPLogConstants.ResponseStatus mStatus;

        /**
         * Setter.
         *
         * @param code response (error) code.
         * @return this.
         */
        public ResponseDescriptor setResponseCode(final String code) {
            this.mCode = code;
            return this;
        }

        /**
         * Setter.
         *
         * @param description response description.
         * @return this.
         */
        public ResponseDescriptor setResponseDescription(final String description) {
            this.mDescription = description;
            return this;
        }

        /**
         * Setter.
         *
         * @param severity response outcome severity.
         * @return this.
         */
        public ResponseDescriptor setResponseSeverity(final Level severity) {
            this.mSeverity = severity;
            return this;
        }

        /**
         * Setter.
         *
         * @param status response overall status.
         * @return this.
         */
        public ResponseDescriptor setResponseStatus(final ONAPLogConstants.ResponseStatus status) {
            this.mStatus = status;
            return this;
        }

        /**
         * Overrideable method to set MDCs based on property values.
         */
        protected void setMDCs() {
            MDC.put(ONAPLogConstants.MDCs.RESPONSE_CODE, defaultToEmpty(this.mCode));
            MDC.put(ONAPLogConstants.MDCs.RESPONSE_DESCRIPTION, defaultToEmpty(this.mDescription));
            MDC.put(ONAPLogConstants.MDCs.RESPONSE_SEVERITY, defaultToEmpty(this.mSeverity));
            MDC.put(ONAPLogConstants.MDCs.RESPONSE_STATUS_CODE, defaultToEmpty(this.mStatus));
        }
    }

    /**
     * Adapter for reading information from an incoming HTTP request.
     *
     * <p>Incoming is generally easy, because in most cases you'll be able to
     * get your hands on the <tt>HttpServletRequest</tt>.</p>
     *
     * <p>Perhaps should be generalized to refer to constants instead of
     * requiring the implementation of specific methods.</p>
     *
     * @param <T> type, for chaining.
     */
    public interface RequestAdapter<T extends RequestAdapter> {

        /**
         * Get header by name.
         * @param name header name.
         * @return header value, or null.
         */
        String getHeader(String name);

        /**
         * Get client address.
         * @return address, if available.
         */
        String getClientAddress();

        /**
         * Get server address.
         * @return address, if available.
         */
        String getServerAddress();

        /**
         * Get default service name, from service URI.
         * @return service name default.
         */
        String getRequestURI();
    }

    /**
     * Default {@link RequestBuilder} impl for {@link HttpServletRequest}, which
     * will should available for most incoming REST requests.
     */
    public static class HttpServletRequestAdapter implements RequestAdapter<HttpServletRequestAdapter> {

        /** Wrapped HTTP request. */
        private final HttpServletRequest mRequest;

        /**
         * Construct adapter for HTTP request.
         * @param request to be wrapped;
         */
        public HttpServletRequestAdapter(final HttpServletRequest request) {
            this.mRequest = checkNotNull(request);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String getHeader(final String name) {
            return this.mRequest.getHeader(name);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String getClientAddress() {
            return this.mRequest.getRemoteAddr();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String getServerAddress() {
            return this.mRequest.getServerName();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String getRequestURI() {
            return this.mRequest.getRequestURI();
        }
    }

    /**
     * Header builder, which (unlike {@link RequestAdapter} will tend to
     * vary a lot from caller to caller, since they each get to choose their
     * own REST (or HTTP, or whatever) client APIs.
     *
     * <p>No default implementation, because there's no HTTP client that's
     * sufficiently ubiquitous to warrant incurring a mandatory dependency.</p>
     *
     * @param <T> type, for chaining.
     */
    public interface RequestBuilder<T extends RequestBuilder> {

        /**
         * Set HTTP header.
         * @param name header name.
         * @param value header value.
         * @return this.
         */
        T setHeader(String name, String value);
    }
}