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 }