2150e28905a8cfbee60116324f1e6cb3aa11b652
[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.io.PrintStream;
11 import java.util.ArrayList;
12 import java.util.HashMap;
13 import java.util.List;
14 import java.util.Map;
15 import java.util.Objects;
16 import java.util.Set;
17 import javax.annotation.Nonnull;
18 import org.opendaylight.controller.md.sal.common.api.data.AsyncDataBroker;
19 import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
20 import org.opendaylight.controller.md.sal.common.api.data.TransactionChainListener;
21 import org.opendaylight.controller.md.sal.dom.api.DOMDataBroker;
22 import org.opendaylight.controller.md.sal.dom.api.DOMDataBrokerExtension;
23 import org.opendaylight.controller.md.sal.dom.api.DOMDataChangeListener;
24 import org.opendaylight.controller.md.sal.dom.api.DOMDataReadOnlyTransaction;
25 import org.opendaylight.controller.md.sal.dom.api.DOMDataReadWriteTransaction;
26 import org.opendaylight.controller.md.sal.dom.api.DOMDataTreeChangeListener;
27 import org.opendaylight.controller.md.sal.dom.api.DOMDataTreeChangeService;
28 import org.opendaylight.controller.md.sal.dom.api.DOMDataTreeIdentifier;
29 import org.opendaylight.controller.md.sal.dom.api.DOMDataWriteTransaction;
30 import org.opendaylight.controller.md.sal.dom.api.DOMTransactionChain;
31 import org.opendaylight.controller.md.sal.trace.api.TracingDOMDataBroker;
32 import org.opendaylight.controller.md.sal.trace.closetracker.impl.CloseTracked;
33 import org.opendaylight.controller.md.sal.trace.closetracker.impl.CloseTrackedRegistry;
34 import org.opendaylight.controller.md.sal.trace.closetracker.impl.CloseTrackedRegistryReportEntry;
35 import org.opendaylight.mdsal.binding.dom.codec.api.BindingNormalizedNodeSerializer;
36 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.mdsaltrace.rev160908.Config;
37 import org.opendaylight.yangtools.concepts.ListenerRegistration;
38 import org.opendaylight.yangtools.yang.binding.DataObject;
39 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
40 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 @SuppressWarnings("checkstyle:JavadocStyle")
45 //...because otherwise it whines about the elements in the @code block even though it's completely valid Javadoc
46
47 /**
48  * TracingBroker logs "write" operations and listener registrations to the md-sal. It logs the instance identifier path,
49  * the objects themselves, as well as the stack trace of the call invoking the registration or write operation.
50  * It works by operating as a "bump on the stack" between the application and actual DataBroker, intercepting write
51  * and registration calls and writing to the log.
52  *
53  * <p>In addition, it (optionally) can also keep track of the stack trace of all new transaction allocations
54  * (including TransactionChains, and transactions created in turn from them), in order to detect and report leaks
55  * from transactions which were not closed.
56  *
57  * <h1>Wiring:</h1>
58  * TracingBroker is designed to be easy to use. In fact, for bundles using Blueprint to inject their DataBroker
59  * TracingBroker can be used without modifying your code at all in two simple steps:
60  * <ol>
61  * <li>
62  * Simply add the dependency "mdsaltrace-features" to
63  * your Karaf pom:
64  * <pre>
65  * {@code
66  *  <dependency>
67  *    <groupId>org.opendaylight.controller</groupId>
68  *    <artifactId>features-mdsal-trace</artifactId>
69  *    <version>1.7.0-SNAPSHOT</version>
70  *    <classifier>features</classifier>
71  *    <type>xml</type>
72  *    <scope>runtime</scope>
73  *  </dependency>
74  * }
75  * </pre>
76  * </li>
77  * <li>
78  * Then just "feature:install odl-mdsal-trace" before you install your "real" feature(s) and you're done.
79  * Beware that with Karaf 4 due to <a href="https://bugs.opendaylight.org/show_bug.cgi?id=9068">Bug 9068</a>
80  * you'll probably have to use feature:install's --no-auto-refresh flag when installing your "real" feature.
81  * </li>
82  * </ol>
83  * This works because the mdsaltrace-impl bundle registers its service implementing DOMDataBroker with a higher
84  * rank than sal-binding-broker. As such, any OSGi service lookup for DataBroker will receive the TracingBroker.
85  * <p> </p>
86  * <h1>Avoiding log bloat:</h1>
87  * TracingBroker can be configured to only print registrations or write ops pertaining to certain subtrees of the
88  * md-sal. This can be done in the code via the methods of this class or via a config file. TracingBroker uses a more
89  * convenient but non-standard representation of the instance identifiers. Each instance identifier segment's
90  * class.getSimpleName() is used separated by a '/'.
91  * <p> </p>
92  * <h1>Known issues</h1>
93  * <ul>
94  *     <li>
95  *        Filtering by paths. For some registrations the codec that converts back from the DOM to binding paths is
96  *        busted. As such, an aproximated path is used in the output. For now it is recommended not to use
97  *        watchRegistrations and allow all registrations to be logged.
98  *     </li>
99  * </ul>
100  *
101  */
102 public class TracingBroker implements TracingDOMDataBroker {
103
104     static final Logger LOG = LoggerFactory.getLogger(TracingBroker.class);
105
106     private static final int STACK_TRACE_FIRST_RELEVANT_FRAME = 2;
107
108     private final BindingNormalizedNodeSerializer codec;
109     private final DOMDataBroker delegate;
110     private final List<Watch> registrationWatches = new ArrayList<>();
111     private final List<Watch> writeWatches = new ArrayList<>();
112
113     private final boolean isDebugging;
114     private final CloseTrackedRegistry<TracingTransactionChain> transactionChainsRegistry;
115     private final CloseTrackedRegistry<TracingReadOnlyTransaction> readOnlyTransactionsRegistry;
116     private final CloseTrackedRegistry<TracingWriteTransaction> writeTransactionsRegistry;
117     private final CloseTrackedRegistry<TracingReadWriteTransaction> readWriteTransactionsRegistry;
118
119     private class Watch {
120         final String iidString;
121         final LogicalDatastoreType store;
122
123         Watch(String iidString, LogicalDatastoreType storeOrNull) {
124             this.store = storeOrNull;
125             this.iidString = iidString;
126         }
127
128         private String toIidCompString(YangInstanceIdentifier iid) {
129             StringBuilder builder = new StringBuilder();
130             toPathString(iid, builder);
131             builder.append('/');
132             return builder.toString();
133         }
134
135         private boolean isParent(String parent, String child) {
136             int parentOffset = 0;
137             if (parent.length() > 0 && parent.charAt(0) == '<') {
138                 parentOffset = parent.indexOf('>') + 1;
139             }
140
141             int childOffset = 0;
142             if (child.length() > 0 && child.charAt(0) == '<') {
143                 childOffset = child.indexOf('>') + 1;
144             }
145
146             return child.startsWith(parent.substring(parentOffset), childOffset);
147         }
148
149         public boolean subtreesOverlap(YangInstanceIdentifier iid, LogicalDatastoreType store,
150                                                                 AsyncDataBroker.DataChangeScope scope) {
151             if (this.store != null && !this.store.equals(store)) {
152                 return false;
153             }
154
155             String otherIidString = toIidCompString(iid);
156             switch (scope) {
157                 case BASE:
158                     return isParent(iidString, otherIidString);
159                 case ONE: //for now just treat like SUBTREE, even though it's not
160                 case SUBTREE:
161                     return isParent(iidString, otherIidString) || isParent(otherIidString, iidString);
162                 default:
163                     return false;
164             }
165         }
166
167         public boolean eventIsOfInterest(YangInstanceIdentifier iid, LogicalDatastoreType store) {
168             if (this.store != null && !this.store.equals(store)) {
169                 return false;
170             }
171
172             return isParent(iidString, toPathString(iid));
173         }
174     }
175
176     public TracingBroker(DOMDataBroker delegate, Config config, BindingNormalizedNodeSerializer codec) {
177         this.delegate = Objects.requireNonNull(delegate);
178         this.codec = Objects.requireNonNull(codec);
179         configure(config);
180
181         if (config.isTransactionDebugContextEnabled() != null) {
182             this.isDebugging = config.isTransactionDebugContextEnabled();
183         } else {
184             this.isDebugging = false;
185         }
186         final String db = "DataBroker";
187         this.transactionChainsRegistry     = new CloseTrackedRegistry<>(db, "createTransactionChain()", isDebugging);
188         this.readOnlyTransactionsRegistry  = new CloseTrackedRegistry<>(db, "newReadOnlyTransaction()", isDebugging);
189         this.writeTransactionsRegistry     = new CloseTrackedRegistry<>(db, "newWriteOnlyTransaction()", isDebugging);
190         this.readWriteTransactionsRegistry = new CloseTrackedRegistry<>(db, "newReadWriteTransaction()", isDebugging);
191     }
192
193     private void configure(Config config) {
194         registrationWatches.clear();
195         List<String> paths = config.getRegistrationWatches();
196         if (paths != null) {
197             for (String path : paths) {
198                 watchRegistrations(path, null);
199             }
200         }
201
202         writeWatches.clear();
203         paths = config.getWriteWatches();
204         if (paths != null) {
205             for (String path : paths) {
206                 watchWrites(path, null);
207             }
208         }
209     }
210
211     /**
212      * Log registrations to this subtree of the md-sal.
213      * @param iidString the iid path of the root of the subtree
214      * @param store Which LogicalDataStore? or null for both
215      */
216     public void watchRegistrations(String iidString, LogicalDatastoreType store) {
217         LOG.info("Watching registrations to {} in {}", iidString, store);
218         registrationWatches.add(new Watch(iidString, store));
219     }
220
221     /**
222      * Log writes to this subtree of the md-sal.
223      * @param iidString the iid path of the root of the subtree
224      * @param store Which LogicalDataStore? or null for both
225      */
226     public void watchWrites(String iidString, LogicalDatastoreType store) {
227         LOG.info("Watching writes to {} in {}", iidString, store);
228         Watch watch = new Watch(iidString, store);
229         writeWatches.add(watch);
230     }
231
232     private boolean isRegistrationWatched(YangInstanceIdentifier iid,
233                                                             LogicalDatastoreType store, DataChangeScope scope) {
234         if (registrationWatches.isEmpty()) {
235             return true;
236         }
237
238         for (Watch regInterest : registrationWatches) {
239             if (regInterest.subtreesOverlap(iid, store, scope)) {
240                 return true;
241             }
242         }
243
244         return false;
245     }
246
247     boolean isWriteWatched(YangInstanceIdentifier iid, LogicalDatastoreType store) {
248         if (writeWatches.isEmpty()) {
249             return true;
250         }
251
252         for (Watch watch : writeWatches) {
253             if (watch.eventIsOfInterest(iid, store)) {
254                 return true;
255             }
256         }
257
258         return false;
259     }
260
261     static void toPathString(InstanceIdentifier<? extends DataObject> iid, StringBuilder builder) {
262         for (InstanceIdentifier.PathArgument pathArg : iid.getPathArguments()) {
263             builder.append('/').append(pathArg.getType().getSimpleName());
264         }
265     }
266
267     String toPathString(YangInstanceIdentifier  yiid) {
268         StringBuilder sb = new StringBuilder();
269         toPathString(yiid, sb);
270         return sb.toString();
271     }
272
273
274     private void toPathString(YangInstanceIdentifier yiid, StringBuilder sb) {
275         InstanceIdentifier<?> iid = codec.fromYangInstanceIdentifier(yiid);
276         if (null == iid) {
277             reconstructIidPathString(yiid, sb);
278         } else {
279             toPathString(iid, sb);
280         }
281     }
282
283     private void reconstructIidPathString(YangInstanceIdentifier yiid, StringBuilder sb) {
284         sb.append("<RECONSTRUCTED FROM: \"").append(yiid.toString()).append("\">");
285         for (YangInstanceIdentifier.PathArgument pathArg : yiid.getPathArguments()) {
286             if (pathArg instanceof YangInstanceIdentifier.AugmentationIdentifier) {
287                 sb.append('/').append("AUGMENTATION");
288                 continue;
289             }
290             sb.append('/').append(pathArg.getNodeType().getLocalName());
291         }
292     }
293
294     String getStackSummary() {
295         StackTraceElement[] stack = Thread.currentThread().getStackTrace();
296
297         StringBuilder sb = new StringBuilder();
298         for (int i = STACK_TRACE_FIRST_RELEVANT_FRAME; i < stack.length; i++) {
299             StackTraceElement frame = stack[i];
300             sb.append("\n\t(TracingBroker)\t").append(frame.getClassName()).append('.').append(frame.getMethodName());
301         }
302
303         return sb.toString();
304     }
305
306     @Override
307     public DOMDataReadWriteTransaction newReadWriteTransaction() {
308         return new TracingReadWriteTransaction(delegate.newReadWriteTransaction(), this, readWriteTransactionsRegistry);
309     }
310
311     @Override
312     public DOMDataWriteTransaction newWriteOnlyTransaction() {
313         return new TracingWriteTransaction(delegate.newWriteOnlyTransaction(), this, writeTransactionsRegistry);
314     }
315
316     @Override
317     public ListenerRegistration<DOMDataChangeListener> registerDataChangeListener(
318                                                         LogicalDatastoreType store, YangInstanceIdentifier yiid,
319                                                         DOMDataChangeListener listener, DataChangeScope scope) {
320         if (isRegistrationWatched(yiid, store, scope)) {
321             LOG.warn("Registration (registerDataChangeListener) for {} from {}",
322                     toPathString(yiid), getStackSummary());
323         }
324         return delegate.registerDataChangeListener(store, yiid, listener, scope);
325     }
326
327     @Override
328     public DOMTransactionChain createTransactionChain(TransactionChainListener transactionChainListener) {
329         return new TracingTransactionChain(
330                 delegate.createTransactionChain(transactionChainListener), this, transactionChainsRegistry);
331     }
332
333     @Override
334     public DOMDataReadOnlyTransaction newReadOnlyTransaction() {
335         return new TracingReadOnlyTransaction(delegate.newReadOnlyTransaction(), this, readOnlyTransactionsRegistry);
336     }
337
338     @Nonnull
339     @Override
340     public Map<Class<? extends DOMDataBrokerExtension>, DOMDataBrokerExtension> getSupportedExtensions() {
341         Map<Class<? extends DOMDataBrokerExtension>, DOMDataBrokerExtension> res = delegate.getSupportedExtensions();
342         DOMDataTreeChangeService treeChangeSvc = (DOMDataTreeChangeService) res.get(DOMDataTreeChangeService.class);
343         if (treeChangeSvc == null) {
344             return res;
345         }
346
347         res = new HashMap<>(res);
348
349         res.put(DOMDataTreeChangeService.class, new DOMDataTreeChangeService() {
350             @Nonnull
351             @Override
352             public <L extends DOMDataTreeChangeListener> ListenerRegistration<L> registerDataTreeChangeListener(
353                     @Nonnull DOMDataTreeIdentifier domDataTreeIdentifier, @Nonnull L listener) {
354                 if (isRegistrationWatched(domDataTreeIdentifier.getRootIdentifier(),
355                         domDataTreeIdentifier.getDatastoreType(), DataChangeScope.SUBTREE)) {
356                     LOG.warn("Registration (registerDataTreeChangeListener) for {} from {}",
357                             toPathString(domDataTreeIdentifier.getRootIdentifier()), getStackSummary());
358                 }
359                 return treeChangeSvc.registerDataTreeChangeListener(domDataTreeIdentifier, listener);
360             }
361         });
362
363         return res;
364     }
365
366     @Override
367     public boolean printOpenTransactions(PrintStream ps) {
368         if (transactionChainsRegistry.getAllUnique().isEmpty()
369             && readOnlyTransactionsRegistry.getAllUnique().isEmpty()
370             && writeTransactionsRegistry.getAllUnique().isEmpty()
371             && readWriteTransactionsRegistry.getAllUnique().isEmpty()) {
372
373             return false;
374         }
375
376         ps.println(getClass().getSimpleName() + " found some not yet (or never..) closed transaction[chain]s!");
377         ps.println("[NB: If no stack traces are shown below, then "
378                  + "enable transaction-debug-context-enabled in mdsaltrace_config.xml]");
379         ps.println();
380         printRegistryOpenTransactions(readOnlyTransactionsRegistry, ps, "  ");
381         printRegistryOpenTransactions(writeTransactionsRegistry, ps, "  ");
382         printRegistryOpenTransactions(readWriteTransactionsRegistry, ps, "  ");
383
384         // Now print details for each non-closed TransactionChain
385         // incl. in turn each ones own read/Write[Only]TransactionsRegistry
386         Set<CloseTrackedRegistryReportEntry<TracingTransactionChain>>
387             entries = transactionChainsRegistry.getAllUnique();
388         if (!entries.isEmpty()) {
389             ps.println("  " + transactionChainsRegistry.getAnchor() + " : "
390                     + transactionChainsRegistry.getCreateDescription());
391         }
392         entries.forEach(entry -> {
393             ps.println("    " + entry.getNumberAddedNotRemoved() + "x TransactionChains opened but not closed here:");
394             entry.getStackTraceElements().forEach(line -> ps.println("      " + line));
395             @SuppressWarnings("resource")
396             TracingTransactionChain txChain = (TracingTransactionChain) entry
397                 .getExampleCloseTracked().getRealCloseTracked();
398             printRegistryOpenTransactions(txChain.getReadOnlyTransactionsRegistry(), ps, "        ");
399             printRegistryOpenTransactions(txChain.getWriteTransactionsRegistry(), ps, "        ");
400             printRegistryOpenTransactions(txChain.getReadWriteTransactionsRegistry(), ps, "        ");
401         });
402         ps.println();
403
404         return true;
405     }
406
407     private <T extends CloseTracked<T>> void printRegistryOpenTransactions(
408             CloseTrackedRegistry<T> registry, PrintStream ps, String indent) {
409         Set<CloseTrackedRegistryReportEntry<T>> entries = registry.getAllUnique();
410         if (!entries.isEmpty()) {
411             ps.println(indent + registry.getAnchor() + " : " + registry.getCreateDescription());
412         }
413         entries.forEach(entry -> {
414             ps.println(indent + "  " + entry.getNumberAddedNotRemoved()
415                 + "x transactions opened here, which are not closed:");
416             entry.getStackTraceElements().forEach(line -> ps.println(indent + "    " + line));
417         });
418         if (!entries.isEmpty()) {
419             ps.println();
420         }
421     }
422 }