View Javadoc
1   package com.atlassian.plugin.instrumentation;
2   
3   import com.atlassian.instrumentation.DefaultInstrumentRegistry;
4   import com.atlassian.instrumentation.InstrumentRegistry;
5   import com.atlassian.instrumentation.RegistryConfiguration;
6   import com.atlassian.instrumentation.operations.OpTimer;
7   import com.atlassian.instrumentation.operations.SimpleOpTimerFactory;
8   import com.google.common.annotations.VisibleForTesting;
9   import org.slf4j.Logger;
10  import org.slf4j.LoggerFactory;
11  
12  import javax.annotation.Nonnull;
13  import java.io.File;
14  import java.time.LocalDateTime;
15  import java.time.format.DateTimeFormatter;
16  import java.util.Optional;
17  
18  import static com.google.common.base.Preconditions.checkNotNull;
19  
20  /**
21   * Optional instrumentation provider for plugin system internals.
22   * <p>
23   * The instrumentation may be safely invoked in any plugin system code, however will no-op unless this provider is
24   * enabled, which requires the following conditions to be met:
25   * <ol>
26   * <li>{@link InstrumentRegistry} is present in the class loader</li>
27   * <li>The boolean system property <code>com.atlassian.plugin.instrumentation.PluginSystemInstrumentation.enabled</code> has been set</li>
28   * </ol>
29   * It is thus up to the application to provide the instrumentation classes and explicitly enable this instrumentation.
30   * <p>
31   * Note to maintainers: extreme care must be taken to ensure that instrumentation classes are not accessed at runtime if
32   * they are not present.
33   *
34   * @since 4.1
35   */
36  public class PluginSystemInstrumentation {
37      private static final Logger log = LoggerFactory.getLogger(PluginSystemInstrumentation.class);
38  
39      public static final String INSTRUMENT_REGISTRY_CLASS = "com.atlassian.instrumentation.InstrumentRegistry";
40      public static final String REGISTRY_NAME = "plugin.system";
41      public static final File REGISTRY_HOME_DIRECTORY = new File(System.getProperty("java.io.tmpdir"));
42  
43      private static final String SINGLE_TIMER_NAME_FORMAT = "%s.%s"; // "<name>.<date>"
44      private static final DateTimeFormatter timerDateFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
45  
46      private final Optional<InstrumentRegistryProxy> instrumentRegistryProxy;
47  
48      /**
49       * Singleton accessor
50       *
51       * @return one and only instance
52       */
53      @Nonnull
54      public static PluginSystemInstrumentation instance() {
55          return LazyHolder.INSTANCE;
56      }
57  
58      private static class LazyHolder {
59          private static final PluginSystemInstrumentation INSTANCE = new PluginSystemInstrumentation();
60      }
61  
62      /**
63       * Name of the system property that enables instrumentation. Note that instrumentation defaults to "off".
64       *
65       * @return property name
66       */
67      @Nonnull
68      public static String getEnabledProperty() {
69          return PluginSystemInstrumentation.class.getName() + ".enabled";
70      }
71  
72      /**
73       * Private constructor for use by {#instance}
74       */
75      @VisibleForTesting
76      PluginSystemInstrumentation() {
77  
78          // class must be present
79          boolean instrumentationPresent;
80          try {
81              Class.forName(INSTRUMENT_REGISTRY_CLASS);
82              instrumentationPresent = true;
83          } catch (ClassNotFoundException e) {
84              instrumentationPresent = false;
85          }
86  
87          // system property to enable instrumentation must also be present
88          Boolean instrumentationEnabled = Boolean.getBoolean(getEnabledProperty());
89          if (!instrumentationPresent && instrumentationEnabled)
90          {
91              log.warn("Instrumentation class ({}) not found. Instrumentation cannot be enabled", INSTRUMENT_REGISTRY_CLASS);
92          }
93  
94          if (instrumentationPresent && instrumentationEnabled) {
95              log.info("Plugin System instrumentation ENABLED via system property '{}'", getEnabledProperty());
96              instrumentRegistryProxy = Optional.of(new InstrumentRegistryProxy());
97          } else {
98              instrumentRegistryProxy = Optional.empty();
99          }
100     }
101 
102     /**
103      * Retrieve the instrument registry if instrumentation is enabled and present in the classloader.
104      *
105      * @return one and only registry
106      */
107     @Nonnull
108     public Optional<InstrumentRegistry> getInstrumentRegistry() {
109         return instrumentRegistryProxy
110                 .map(p -> Optional.of(p.getInstrumentRegistry()))
111                 .orElse(Optional.empty());
112     }
113 
114     /**
115      * Pull a timer from the instrument registry, if instrumentation is enabled and present in the classloader.
116      * <p>
117      * This timer records wall-clock time and CPU time on the thread it was instantiated from.
118      * <p>
119      * This should be used for a section of code that we wish to repeatedly measure and aggregate results for.
120      * <p>
121      * The timer is {@link java.io.Closeable} and is expected to be used as per the following example:
122      * <pre>
123      * try (Timer ignored = PluginSystemInstrumentation.instance().pullTimer("getEnabledModuleDescriptorsByClass")) {
124      *     // block we wish to repeatedly measure
125      * }
126      * </pre>
127      *
128      * @param name that the timer will report
129      * @return timer that may be empty - if no instrumentation it is still safe to close, which is a no-op
130      * @see #pullSingleTimer(String)
131      */
132     @Nonnull
133     public Timer pullTimer(@Nonnull final String name) {
134         return new Timer(instrumentRegistryProxy
135                 .map(p -> Optional.of(p.pullTimer(checkNotNull(name))))
136                 .orElse(Optional.empty()));
137     }
138 
139     /**
140      * Pull a timer from the instrument registry, if instrumentation is enabled and present in the classloader.
141      * <p>
142      * This timer records wall-clock time and CPU time on the thread it was instantiated from.
143      * <p>
144      * This should be used for a section of code that we wish to measure once - the date and time appended to its name.
145      * <p>
146      * The timer is {@link java.io.Closeable} and is expected to be used as per the following example:
147      * <pre>
148      * try (Timer ignored = PluginSystemInstrumentation.instance().pullSingleTimer("earlyStartup")) {
149      *     // block we wish to measure once, with a unique name
150      * }
151      * </pre>
152      *
153      * @param name that the timer will report
154      * @return timer that may be empty - if no instrumentation it is still safe to close, which is a no-op
155      * @see #pullTimer(String)
156      */
157     @Nonnull
158     public SingleTimer pullSingleTimer(@Nonnull final String name) {
159         //This implementation looks like it could be replaced by a simple .map(...), but doing so leads to a
160         //NoClassDefFoundError if OpTimer is not on the classpath. See the javadoc on Timer for details
161         return new SingleTimer(instrumentRegistryProxy
162                 .map(p -> Optional.of(p.pullTimer(formatSingleName(checkNotNull(name)))))
163                 .orElse(Optional.empty()), name);
164     }
165 
166     /**
167      * Format a user supplied name by appending the date.
168      *
169      * @param name user supplied
170      * @return formatted name, with date appended after a single "."
171      */
172     @Nonnull
173     private String formatSingleName(@Nonnull final String name) {
174         return String.format(SINGLE_TIMER_NAME_FORMAT, checkNotNull(name), timerDateFormatter.format(LocalDateTime.now()));
175     }
176 
177     /**
178      * Proxy to wrap up the actual {@link InstrumentRegistry}.
179      * <p>
180      * It is necessary as this may not be present in the class loader, hence we only want to reference it if we've
181      * already checked for its presence.
182      */
183     private class InstrumentRegistryProxy {
184         final InstrumentRegistry instrumentRegistry = new DefaultInstrumentRegistry(
185                 new SimpleOpTimerFactory(),
186                 new RegistryConfiguration() {
187                     @Override
188                     public String getRegistryName() {
189                         return REGISTRY_NAME;
190                     }
191 
192                     @Override
193                     public boolean isCPUCostCollected() {
194                         return true;
195                     }
196 
197                     @Override
198                     public File getRegistryHomeDirectory() {
199                         return REGISTRY_HOME_DIRECTORY;
200                     }
201                 });
202 
203         @Nonnull
204         InstrumentRegistry getInstrumentRegistry() {
205             return instrumentRegistry;
206         }
207 
208         @Nonnull
209         OpTimer pullTimer(@Nonnull final String name) {
210             return instrumentRegistry.pullTimer(checkNotNull(name));
211         }
212     }
213 }