001/*-
002 * #%L
003 * HAPI FHIR Subscription Server
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.subscription.util;
021
022import ca.uhn.fhir.interceptor.api.Hook;
023import ca.uhn.fhir.interceptor.api.Interceptor;
024import ca.uhn.fhir.interceptor.api.Pointcut;
025import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
026import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
027import ca.uhn.fhir.jpa.subscription.model.ResourceDeliveryMessage;
028import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
029import ca.uhn.fhir.util.StopWatch;
030import org.slf4j.Logger;
031import org.slf4j.LoggerFactory;
032import org.slf4j.event.Level;
033
034import java.util.Date;
035import java.util.EnumMap;
036import java.util.function.Function;
037
038/**
039 * This interceptor can be used for troubleshooting subscription processing. It provides very
040 * detailed logging about the subscription processing pipeline.
041 * <p>
042 * This interceptor logs each step in the processing pipeline with a
043 * different event code, using the event codes itemized in
044 * {@link EventCodeEnum}. By default these are each placed in a logger with
045 * a different name (e.g. <code>ca.uhn.fhir.jpa.subscription.util.SubscriptionDebugLogInterceptor.SUBS20</code>
046 * in order to facilitate fine-grained logging controls where some codes are omitted and
047 * some are not.
048 * </p>
049 * <p>
050 * A custom log factory can also be passed in, in which case the logging
051 * creation may use another strategy.
052 * </p>
053 *
054 * @see EventCodeEnum
055 * @since 3.7.0
056 */
057@Interceptor
058public class SubscriptionDebugLogInterceptor {
059
060        private static final String SUBSCRIPTION_DEBUG_LOG_INTERCEPTOR_PRECHECK =
061                        "SubscriptionDebugLogInterceptor_precheck";
062        private final Level myLevel;
063        private final EnumMap<EventCodeEnum, Logger> myLoggers;
064
065        /**
066         * Constructor that logs at INFO level to the logger <code>ca.uhn.fhir.jpa.subscription.util.SubscriptionDebugLogInterceptor</code>
067         */
068        public SubscriptionDebugLogInterceptor() {
069                this(defaultLogFactory(), Level.INFO);
070        }
071
072        /**
073         * Constructor using a specific logger
074         */
075        public SubscriptionDebugLogInterceptor(Function<EventCodeEnum, Logger> theLogFactory, Level theLevel) {
076                myLevel = theLevel;
077                myLoggers = new EnumMap<>(EventCodeEnum.class);
078                for (EventCodeEnum next : EventCodeEnum.values()) {
079                        myLoggers.put(next, theLogFactory.apply(next));
080                }
081        }
082
083        @Hook(Pointcut.SUBSCRIPTION_RESOURCE_MODIFIED)
084        public void step10_resourceModified(ResourceModifiedMessage theMessage) {
085                String value = Long.toString(System.currentTimeMillis());
086                theMessage.setAttribute(SUBSCRIPTION_DEBUG_LOG_INTERCEPTOR_PRECHECK, value);
087                String resourceId = theMessage.getPayloadId();
088                if (resourceId == null) {
089                        // Delete operations have no payload
090                        resourceId = theMessage.getId();
091                }
092                log(
093                                EventCodeEnum.SUBS1,
094                                "Resource {} is starting the processing pipeline (op={})",
095                                resourceId,
096                                theMessage.getOperationType());
097        }
098
099        /*
100         * These methods are numbered in the order that an individual
101         * resource would go through them, for clarity and ease of
102         * tracing when debugging and poring over logs.
103         *
104         * I don't know if this numbering scheme makes sense.. I'm incrementing
105         * by 10 for each step in the normal delivery pipeline, leaving lots of
106         * gaps to add things if we ever need them.
107         */
108
109        @Hook(Pointcut.SUBSCRIPTION_BEFORE_PERSISTED_RESOURCE_CHECKED)
110        public void step20_beforeChecked(ResourceModifiedMessage theMessage) {
111                log(
112                                EventCodeEnum.SUBS2,
113                                "Checking resource {} (op={}) for matching subscriptions",
114                                theMessage.getPayloadId(),
115                                theMessage.getOperationType());
116        }
117
118        @Hook(Pointcut.SUBSCRIPTION_RESOURCE_MATCHED)
119        public void step30_subscriptionMatched(ResourceDeliveryMessage theMessage, InMemoryMatchResult theResult) {
120                log(
121                                EventCodeEnum.SUBS3,
122                                "Resource {} matched by subscription {} (memory match={})",
123                                theMessage.getPayloadId(),
124                                theMessage.getSubscription().getIdElementString(),
125                                theResult.isInMemory());
126        }
127
128        @Hook(Pointcut.SUBSCRIPTION_RESOURCE_DID_NOT_MATCH_ANY_SUBSCRIPTIONS)
129        public void step35_subscriptionNotMatched(ResourceModifiedMessage theMessage) {
130                log(EventCodeEnum.SUBS4, "Resource {} did not match any subscriptions", theMessage.getPayloadId());
131        }
132
133        @Hook(Pointcut.SUBSCRIPTION_BEFORE_DELIVERY)
134        public void step40_beforeDelivery(ResourceDeliveryMessage theMessage) {
135                log(
136                                EventCodeEnum.SUBS5,
137                                "Delivering resource {} for subscription {} to channel of type {} to endpoint {}",
138                                theMessage.getPayloadId(),
139                                theMessage.getSubscription().getIdElementString(),
140                                theMessage.getSubscription().getChannelType(),
141                                theMessage.getSubscription().getEndpointUrl());
142        }
143
144        @Hook(Pointcut.SUBSCRIPTION_AFTER_DELIVERY_FAILED)
145        public void step45_deliveryFailed(ResourceDeliveryMessage theMessage, Exception theFailure) {
146                String payloadId = null;
147                String subscriptionId = null;
148                CanonicalSubscriptionChannelType channelType = null;
149                String failureString = null;
150                if (theMessage != null) {
151                        payloadId = theMessage.getPayloadId();
152                        if (theMessage.getSubscription() != null) {
153                                subscriptionId = theMessage.getSubscription().getIdElementString();
154                                channelType = theMessage.getSubscription().getChannelType();
155                        }
156                }
157                if (theFailure != null) {
158                        failureString = theFailure.toString();
159                }
160                log(
161                                EventCodeEnum.SUBS6,
162                                "Delivery of resource {} for subscription {} to channel of type {} - Failure: {}",
163                                payloadId,
164                                subscriptionId,
165                                channelType,
166                                failureString);
167        }
168
169        @Hook(Pointcut.SUBSCRIPTION_AFTER_DELIVERY)
170        public void step50_afterDelivery(ResourceDeliveryMessage theMessage) {
171                String processingTime = theMessage
172                                .getAttribute(SUBSCRIPTION_DEBUG_LOG_INTERCEPTOR_PRECHECK)
173                                .map(Long::parseLong)
174                                .map(Date::new)
175                                .map(start -> new StopWatch(start).toString())
176                                .orElse("(unknown)");
177
178                log(
179                                EventCodeEnum.SUBS7,
180                                "Finished delivery of resource {} for subscription {} to channel of type {} - Total processing time: {}",
181                                theMessage.getPayloadId(),
182                                theMessage.getSubscription().getIdElementString(),
183                                theMessage.getSubscription().getChannelType(),
184                                processingTime);
185        }
186
187        protected void log(EventCodeEnum theEventCode, String theMessage, Object... theArguments) {
188                Logger logger = myLoggers.get(theEventCode);
189                if (logger != null) {
190                        switch (myLevel) {
191                                case ERROR:
192                                        logger.error(theMessage, theArguments);
193                                        break;
194                                case WARN:
195                                        logger.warn(theMessage, theArguments);
196                                        break;
197                                case INFO:
198                                        logger.info(theMessage, theArguments);
199                                        break;
200                                case DEBUG:
201                                        logger.debug(theMessage, theArguments);
202                                        break;
203                                case TRACE:
204                                        logger.trace(theMessage, theArguments);
205                                        break;
206                        }
207                }
208        }
209
210        public enum EventCodeEnum {
211                /**
212                 * A new/updated resource has been submitted to the processing pipeline and is about
213                 * to be placed on the matchign queue.
214                 */
215                SUBS1,
216                /**
217                 * A resources has been dequeued from the matching queue and is about to be checked
218                 * for any matching subscriptions.
219                 */
220                SUBS2,
221                /**
222                 * The resource has matched a subscription (logged once for each matching subscription)
223                 * and is about to be queued for delivery.
224                 */
225                SUBS3,
226                /**
227                 * The resource did not match any subscriptions and processing is complete.
228                 */
229                SUBS4,
230                /**
231                 * The resource has been dequeued from the delivery queue and is about to be
232                 * delivered.
233                 */
234                SUBS5,
235                /**
236                 * Delivery failed
237                 */
238                SUBS6,
239                /**
240                 * Delivery is now complete and processing is finished.
241                 */
242                SUBS7
243        }
244
245        private static Function<EventCodeEnum, Logger> defaultLogFactory() {
246                return code -> LoggerFactory.getLogger(SubscriptionDebugLogInterceptor.class.getName() + "." + code.name());
247        }
248}