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