Bug 9034: TracingBroker with TracingReadOnlyTransaction
[controller.git] / opendaylight / md-sal / mdsal-trace / dom-impl / src / main / java / org / opendaylight / controller / md / sal / trace / dom / impl / TracingBroker.java
1 /*
2  * Copyright (c) 2016 Red Hat, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.controller.md.sal.trace.dom.impl;
9
10 import java.util.ArrayList;
11 import java.util.HashMap;
12 import java.util.List;
13 import java.util.Map;
14 import java.util.Objects;
15 import javax.annotation.Nonnull;
16
17 import org.opendaylight.controller.md.sal.common.api.data.AsyncDataBroker;
18 import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
19 import org.opendaylight.controller.md.sal.common.api.data.TransactionChainListener;
20 import org.opendaylight.controller.md.sal.dom.api.DOMDataBroker;
21 import org.opendaylight.controller.md.sal.dom.api.DOMDataBrokerExtension;
22 import org.opendaylight.controller.md.sal.dom.api.DOMDataChangeListener;
23 import org.opendaylight.controller.md.sal.dom.api.DOMDataReadOnlyTransaction;
24 import org.opendaylight.controller.md.sal.dom.api.DOMDataReadWriteTransaction;
25 import org.opendaylight.controller.md.sal.dom.api.DOMDataTreeChangeListener;
26 import org.opendaylight.controller.md.sal.dom.api.DOMDataTreeChangeService;
27 import org.opendaylight.controller.md.sal.dom.api.DOMDataTreeIdentifier;
28 import org.opendaylight.controller.md.sal.dom.api.DOMDataWriteTransaction;
29 import org.opendaylight.controller.md.sal.dom.api.DOMTransactionChain;
30 import org.opendaylight.controller.md.sal.trace.api.TracingDOMDataBroker;
31 import org.opendaylight.mdsal.binding.dom.codec.api.BindingNormalizedNodeSerializer;
32 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.mdsaltrace.rev160908.Config;
33 import org.opendaylight.yangtools.concepts.ListenerRegistration;
34 import org.opendaylight.yangtools.yang.binding.DataObject;
35 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
36 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40 @SuppressWarnings("checkstyle:JavadocStyle")
41 //...because otherwise it whines about the elements in the @code block even though it's completely valid Javadoc
42
43 /**
44  * TracingBroker logs "write" operations and listener registrations to the md-sal. It logs the instance identifier path,
45  * the objects themselves, as well as the stack trace of the call invoking the registration or write operation.
46  * It works by operating as a "bump on the stack" between the application and actual DataBroker, intercepting write
47  * and registration calls and writing to the log.
48  * <h1>Wiring:</h1>
49  * TracingBroker is designed to be easy to use. In fact, for bundles using Blueprint to inject their DataBroker
50  * TracingBroker can be used without modifying your code at all in two simple steps:
51  * <ol>
52  * <li>
53  * Simply add the dependency "mdsaltrace-features" to
54  * your karaf pom:
55  * <pre>
56  * {@code
57  *  <dependency>
58  *    <groupId>org.opendaylight.controller</groupId>
59  *    <artifactId>mdsal-trace-features</artifactId>
60  *    <classifier>features</classifier>
61  *    <type>xml</type>
62  *    <scope>runtime</scope>
63  *    <version>0.1.6-SNAPSHOT</version>
64  *  </dependency>
65  * }
66  * </pre>
67  * </li>
68  * <li>
69  * Then just load the odl-mdsal-trace feature before your feature and you're done.
70  * </li>
71  * </ol>
72  * This works because the mdsaltrace-impl bundle registers its service implementing DOMDataBroker with a higher
73  * rank than sal-binding-broker. As such, any OSGi service lookup for DataBroker will receive the TracingBroker.
74  * <p> </p>
75  * <h1>Avoiding log bloat:</h1>
76  * TracingBroker can be configured to only print registrations or write ops pertaining to certain subtrees of the
77  * md-sal. This can be done in the code via the methods of this class or via a config file. TracingBroker uses a more
78  * convenient but non-standard representation of the instance identifiers. Each instance identifier segment's
79  * class.getSimpleName() is used separated by a '/'.
80  * <p> </p>
81  * <h1>Known issues</h1>
82  * <ul>
83  *     <li>
84  *        Filtering by paths. For some registrations the codec that converts back from the DOM to binding paths is
85  *        busted. As such, an aproximated path is used in the output. For now it is recommended not to use
86  *        watchRegistrations and allow all registrations to be logged.
87  *     </li>
88  * </ul>
89  *
90  */
91 public class TracingBroker implements TracingDOMDataBroker {
92
93     static final Logger LOG = LoggerFactory.getLogger(TracingBroker.class);
94
95     private static final int STACK_TRACE_FIRST_RELEVANT_FRAME = 2;
96
97     private final BindingNormalizedNodeSerializer codec;
98     private final DOMDataBroker delegate;
99     private final List<Watch> registrationWatches = new ArrayList<>();
100     private final List<Watch> writeWatches = new ArrayList<>();
101
102
103     private class Watch {
104         final String iidString;
105         final LogicalDatastoreType store;
106
107         Watch(String iidString, LogicalDatastoreType storeOrNull) {
108             this.store = storeOrNull;
109             this.iidString = iidString;
110         }
111
112         private String toIidCompString(YangInstanceIdentifier iid) {
113             StringBuilder builder = new StringBuilder();
114             toPathString(iid, builder);
115             builder.append('/');
116             return builder.toString();
117         }
118
119         private boolean isParent(String parent, String child) {
120             int parentOffset = 0;
121             if (parent.length() > 0 && parent.charAt(0) == '<') {
122                 parentOffset = parent.indexOf('>') + 1;
123             }
124
125             int childOffset = 0;
126             if (child.length() > 0 && child.charAt(0) == '<') {
127                 childOffset = child.indexOf('>') + 1;
128             }
129
130             return child.startsWith(parent.substring(parentOffset), childOffset);
131         }
132
133         public boolean subtreesOverlap(YangInstanceIdentifier iid, LogicalDatastoreType store,
134                                                                 AsyncDataBroker.DataChangeScope scope) {
135             if (this.store != null && !this.store.equals(store)) {
136                 return false;
137             }
138
139             String otherIidString = toIidCompString(iid);
140             switch (scope) {
141                 case BASE:
142                     return isParent(iidString, otherIidString);
143                 case ONE: //for now just treat like SUBTREE, even though it's not
144                 case SUBTREE:
145                     return isParent(iidString, otherIidString) || isParent(otherIidString, iidString);
146                 default:
147                     return false;
148             }
149         }
150
151         public boolean eventIsOfInterest(YangInstanceIdentifier iid, LogicalDatastoreType store) {
152             if (this.store != null && !this.store.equals(store)) {
153                 return false;
154             }
155
156             return isParent(iidString, toPathString(iid));
157         }
158     }
159
160     public TracingBroker(DOMDataBroker delegate, Config config, BindingNormalizedNodeSerializer codec) {
161         this.delegate = Objects.requireNonNull(delegate);
162         this.codec = Objects.requireNonNull(codec);
163         configure(config);
164     }
165
166     private void configure(Config config) {
167         registrationWatches.clear();
168         List<String> paths = config.getRegistrationWatches();
169         if (paths != null) {
170             for (String path : paths) {
171                 watchRegistrations(path, null);
172             }
173         }
174
175         writeWatches.clear();
176         paths = config.getWriteWatches();
177         if (paths != null) {
178             for (String path : paths) {
179                 watchWrites(path, null);
180             }
181         }
182     }
183
184     /**
185      * Log registrations to this subtree of the md-sal.
186      * @param iidString the iid path of the root of the subtree
187      * @param store Which LogicalDataStore? or null for both
188      */
189     public void watchRegistrations(String iidString, LogicalDatastoreType store) {
190         LOG.info("Watching registrations to {} in {}", iidString, store);
191         registrationWatches.add(new Watch(iidString, store));
192     }
193
194     /**
195      * Log writes to this subtree of the md-sal.
196      * @param iidString the iid path of the root of the subtree
197      * @param store Which LogicalDataStore? or null for both
198      */
199     public void watchWrites(String iidString, LogicalDatastoreType store) {
200         LOG.info("Watching writes to {} in {}", iidString, store);
201         Watch watch = new Watch(iidString, store);
202         writeWatches.add(watch);
203     }
204
205     private boolean isRegistrationWatched(YangInstanceIdentifier iid,
206                                                             LogicalDatastoreType store, DataChangeScope scope) {
207         if (registrationWatches.isEmpty()) {
208             return true;
209         }
210
211         for (Watch regInterest : registrationWatches) {
212             if (regInterest.subtreesOverlap(iid, store, scope)) {
213                 return true;
214             }
215         }
216
217         return false;
218     }
219
220     boolean isWriteWatched(YangInstanceIdentifier iid, LogicalDatastoreType store) {
221         if (writeWatches.isEmpty()) {
222             return true;
223         }
224
225         for (Watch watch : writeWatches) {
226             if (watch.eventIsOfInterest(iid, store)) {
227                 return true;
228             }
229         }
230
231         return false;
232     }
233
234     static void toPathString(InstanceIdentifier<? extends DataObject> iid, StringBuilder builder) {
235         for (InstanceIdentifier.PathArgument pathArg : iid.getPathArguments()) {
236             builder.append('/').append(pathArg.getType().getSimpleName());
237         }
238     }
239
240     String toPathString(YangInstanceIdentifier  yiid) {
241         StringBuilder sb = new StringBuilder();
242         toPathString(yiid, sb);
243         return sb.toString();
244     }
245
246
247     void toPathString(YangInstanceIdentifier yiid, StringBuilder sb) {
248         InstanceIdentifier iid = codec.fromYangInstanceIdentifier(yiid);
249         if (null == iid) {
250             reconstructIidPathString(yiid, sb);
251         } else {
252             toPathString(iid, sb);
253         }
254     }
255
256     private void reconstructIidPathString(YangInstanceIdentifier yiid, StringBuilder sb) {
257         sb.append("<RECONSTRUCTED FROM: \"").append(yiid.toString()).append("\">");
258         for (YangInstanceIdentifier.PathArgument pathArg : yiid.getPathArguments()) {
259             if (pathArg instanceof YangInstanceIdentifier.AugmentationIdentifier) {
260                 sb.append('/').append("AUGMENTATION");
261                 continue;
262             }
263             sb.append('/').append(pathArg.getNodeType().getLocalName());
264         }
265     }
266
267     String getStackSummary() {
268         StackTraceElement[] stack = Thread.currentThread().getStackTrace();
269
270         StringBuilder sb = new StringBuilder();
271         for (int i = STACK_TRACE_FIRST_RELEVANT_FRAME; i < stack.length; i++) {
272             StackTraceElement frame = stack[i];
273             sb.append("\n\t(TracingBroker)\t").append(frame.getClassName()).append('.').append(frame.getMethodName());
274         }
275
276         return sb.toString();
277     }
278
279     @Override
280     public DOMDataReadWriteTransaction newReadWriteTransaction() {
281         return new TracingReadWriteTransaction(delegate.newReadWriteTransaction(), this);
282     }
283
284     @Override
285     public DOMDataWriteTransaction newWriteOnlyTransaction() {
286         return new TracingWriteTransaction(delegate.newWriteOnlyTransaction(), this);
287     }
288
289     @Override
290     public ListenerRegistration<DOMDataChangeListener> registerDataChangeListener(
291                                                         LogicalDatastoreType store, YangInstanceIdentifier yiid,
292                                                         DOMDataChangeListener listener, DataChangeScope scope) {
293         if (isRegistrationWatched(yiid, store, scope)) {
294             LOG.warn("Registration (registerDataChangeListener) for {} from {}",
295                     toPathString(yiid), getStackSummary());
296         }
297         return delegate.registerDataChangeListener(store, yiid, listener, scope);
298     }
299
300     @Override
301     public DOMTransactionChain createTransactionChain(TransactionChainListener transactionChainListener) {
302         return new TracingTransactionChain(delegate.createTransactionChain(transactionChainListener), this);
303     }
304
305     @Override
306     public DOMDataReadOnlyTransaction newReadOnlyTransaction() {
307         return new TracingReadOnlyTransaction(delegate.newReadOnlyTransaction(), this);
308     }
309
310     @Nonnull
311     @Override
312     public Map<Class<? extends DOMDataBrokerExtension>, DOMDataBrokerExtension> getSupportedExtensions() {
313         Map<Class<? extends DOMDataBrokerExtension>, DOMDataBrokerExtension> res = delegate.getSupportedExtensions();
314         DOMDataTreeChangeService treeChangeSvc = (DOMDataTreeChangeService) res.get(DOMDataTreeChangeService.class);
315         if (treeChangeSvc == null) {
316             return res;
317         }
318
319         res = new HashMap<>(res);
320
321         res.put(DOMDataTreeChangeService.class, new DOMDataTreeChangeService() {
322             @Nonnull
323             @Override
324             public <L extends DOMDataTreeChangeListener> ListenerRegistration<L> registerDataTreeChangeListener(
325                     @Nonnull DOMDataTreeIdentifier domDataTreeIdentifier, @Nonnull L listener) {
326                 if (isRegistrationWatched(domDataTreeIdentifier.getRootIdentifier(),
327                         domDataTreeIdentifier.getDatastoreType(), DataChangeScope.SUBTREE)) {
328                     LOG.warn("Registration (registerDataTreeChangeListener) for {} from {}",
329                             toPathString(domDataTreeIdentifier.getRootIdentifier()), getStackSummary());
330                 }
331                 return treeChangeSvc.registerDataTreeChangeListener(domDataTreeIdentifier, listener);
332             }
333         });
334
335         return res;
336     }
337 }