View Javadoc
1   /*
2    * Licensed under the GPL License. You may not use this file except in compliance with the License.
3    * You may obtain a copy of the License at
4    *
5    *   https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
6    *
7    * THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
8    * WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR
9    * PURPOSE.
10   */
11  package psiprobe.model.stats;
12  
13  import com.thoughtworks.xstream.XStream;
14  
15  import jakarta.inject.Inject;
16  
17  import java.io.File;
18  import java.io.IOException;
19  import java.io.InputStream;
20  import java.io.OutputStream;
21  import java.nio.file.Files;
22  import java.nio.file.Paths;
23  import java.util.ArrayList;
24  import java.util.Collections;
25  import java.util.HashMap;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Map.Entry;
29  import java.util.TreeMap;
30  
31  import org.jfree.data.xy.XYDataItem;
32  import org.slf4j.Logger;
33  import org.slf4j.LoggerFactory;
34  import org.springframework.beans.factory.DisposableBean;
35  import org.springframework.beans.factory.InitializingBean;
36  import org.springframework.beans.factory.annotation.Value;
37  import org.springframework.context.ApplicationContext;
38  import org.springframework.context.ApplicationContextAware;
39  import org.springframework.web.context.WebApplicationContext;
40  
41  import psiprobe.tools.UpdateCommitLock;
42  
43  /**
44   * The Class StatsCollection.
45   */
46  public class StatsCollection implements InitializingBean, DisposableBean, ApplicationContextAware {
47  
48    /** The Constant logger. */
49    private static final Logger logger = LoggerFactory.getLogger(StatsCollection.class);
50  
51    /** The stats data. */
52    private Map<String, List<XYDataItem>> statsData = new TreeMap<>();
53  
54    /** The xstream. */
55    @Inject
56    private XStream xstream;
57  
58    /** The swap file name. */
59    private String swapFileName;
60  
61    /** The storage path. */
62    private String storagePath;
63  
64    /** The context temp dir. */
65    private File contextTempDir;
66  
67    /** The max files. */
68    private int maxFiles = 2;
69  
70    /** The lock. */
71    private final UpdateCommitLock lock = new UpdateCommitLock();
72  
73    /**
74     * Gets the swap file name.
75     *
76     * @return the swap file name
77     */
78    public String getSwapFileName() {
79      return swapFileName;
80    }
81  
82    /**
83     * Sets the swap file name.
84     *
85     * @param swapFileName the new swap file name
86     */
87    @Value("stats.xml")
88    public void setSwapFileName(String swapFileName) {
89      this.swapFileName = swapFileName;
90    }
91  
92    /**
93     * Gets the storage path.
94     *
95     * @return the storage path
96     */
97    public String getStoragePath() {
98      return storagePath;
99    }
100 
101   /**
102    * Sets the storage path. The default location for the stat files is
103    * $CALALINA_BASE/work/&lt;hostname&gt;/&lt;context_name&gt;. Use this property to override it.
104    *
105    * @param storagePath the new storage path
106    */
107   // TODO We should make this configurable
108   public void setStoragePath(String storagePath) {
109     this.storagePath = storagePath;
110   }
111 
112   /**
113    * Checks if is collected.
114    *
115    * @param statsName the stats name
116    *
117    * @return true, if is collected
118    */
119   public synchronized boolean isCollected(String statsName) {
120     return statsData.get(statsName) != null;
121   }
122 
123   /**
124    * Gets the max files.
125    *
126    * @return the max files
127    */
128   public int getMaxFiles() {
129     return maxFiles;
130   }
131 
132   /**
133    * Sets the max files.
134    *
135    * @param maxFiles the new max files
136    */
137   public void setMaxFiles(int maxFiles) {
138     this.maxFiles = maxFiles > 0 ? maxFiles : 2;
139   }
140 
141   /**
142    * New stats.
143    *
144    * @param name the name
145    * @param maxElements the max elements
146    *
147    * @return the list
148    */
149   public synchronized List<XYDataItem> newStats(String name, int maxElements) {
150     List<XYDataItem> stats = Collections.synchronizedList(new ArrayList<>(maxElements));
151     statsData.put(name, stats);
152     return stats;
153   }
154 
155   /**
156    * Reset stats.
157    *
158    * @param name the name
159    */
160   public synchronized void resetStats(String name) {
161     List<XYDataItem> stats = getStats(name);
162     if (stats != null) {
163       stats.clear();
164     }
165   }
166 
167   /**
168    * Gets the stats.
169    *
170    * @param name the name
171    *
172    * @return the stats
173    */
174   public synchronized List<XYDataItem> getStats(String name) {
175     return statsData.get(name);
176   }
177 
178   /**
179    * Gets the last value for stat.
180    *
181    * @param statName the stat name
182    *
183    * @return the last value for stat
184    */
185   public long getLastValueForStat(String statName) {
186     long statValue = 0;
187 
188     List<XYDataItem> stats = getStats(statName);
189     if (stats != null && !stats.isEmpty()) {
190       XYDataItem xy = stats.get(stats.size() - 1);
191       if (xy != null && xy.getY() != null) {
192         statValue = xy.getY().longValue();
193       }
194     }
195 
196     return statValue;
197   }
198 
199   /**
200    * Returns series if stat name starts with the prefix.
201    *
202    * @param statNamePrefix they key under which the stats are stored
203    *
204    * @return a Map of matching stats. Map keys are stat names and map values are corresponding
205    *         series.
206    */
207   public synchronized Map<String, List<XYDataItem>> getStatsByPrefix(String statNamePrefix) {
208     Map<String, List<XYDataItem>> map = new HashMap<>();
209     for (Map.Entry<String, List<XYDataItem>> en : statsData.entrySet()) {
210       if (en.getKey().startsWith(statNamePrefix)) {
211         map.put(en.getKey(), en.getValue());
212       }
213     }
214     return map;
215   }
216 
217   /**
218    * Make file.
219    *
220    * @return the file
221    */
222   private File makeFile() {
223     return storagePath == null ? new File(contextTempDir, swapFileName)
224         : new File(storagePath, swapFileName);
225   }
226 
227   /**
228    * Shift files.
229    *
230    * @param index the index
231    */
232   private void shiftFiles(int index) {
233     if (index >= maxFiles - 1) {
234       try {
235         if (Files.exists(Paths.get(makeFile().getAbsolutePath() + "." + index))) {
236           Files.delete(Paths.get(makeFile().getAbsolutePath() + "." + index));
237         }
238       } catch (IOException e) {
239         logger.error("Could not delete file {}",
240             new File(makeFile().getAbsolutePath() + "." + index).getName());
241       }
242     } else {
243       shiftFiles(index + 1);
244       File srcFile = index == 0 ? makeFile() : new File(makeFile().getAbsolutePath() + "." + index);
245       if (Files.exists(srcFile.toPath())) {
246         File destFile = new File(makeFile().getAbsolutePath() + "." + (index + 1));
247         if (!srcFile.renameTo(destFile)) {
248           logger.error("Could not rename file {} to {}", srcFile.getName(), destFile.getName());
249         }
250       }
251     }
252   }
253 
254   /**
255    * Writes stats data to file on disk.
256    *
257    * @throws InterruptedException if a lock cannot be obtained
258    */
259   public synchronized void serialize() throws InterruptedException {
260     lock.lockForCommit();
261     long start = System.currentTimeMillis();
262     try {
263       shiftFiles(0);
264       try (OutputStream os = Files.newOutputStream(makeFile().toPath())) {
265         xstream.toXML(statsData, os);
266       }
267     } catch (Exception e) {
268       logger.error("Could not write stats data to '{}'", makeFile().getAbsolutePath(), e);
269     } finally {
270       lock.releaseCommitLock();
271       logger.debug("stats serialized in {}ms", System.currentTimeMillis() - start);
272     }
273   }
274 
275   /**
276    * Deserialize.
277    *
278    * @param file the file
279    *
280    * @return the map
281    */
282   @SuppressWarnings("unchecked")
283   private Map<String, List<XYDataItem>> deserialize(File file) {
284     Map<String, List<XYDataItem>> stats = null;
285     if (file.exists() && file.canRead()) {
286       long start = System.currentTimeMillis();
287       try {
288         try (InputStream fis = Files.newInputStream(file.toPath())) {
289           stats = (Map<String, List<XYDataItem>>) xstream.fromXML(fis);
290 
291           if (stats != null) {
292             // adjust stats data so that charts look realistic.
293             // we do that by ending the previous stats group with 0 value
294             // and starting the current stats group also with 0
295             // thus giving the chart nice plunge to zero indicating downtime
296             // and lets not bother about rotating stats;
297             // regular stats collection cycle will do it
298 
299             for (Entry<String, List<XYDataItem>> set : stats.entrySet()) {
300               List<XYDataItem> list = set.getValue();
301               if (!list.isEmpty()) {
302                 XYDataItem xy = list.get(list.size() - 1);
303                 list.add(new XYDataItem(xy.getX().longValue() + 1, 0));
304                 list.add(new XYDataItem(System.currentTimeMillis(), 0));
305               }
306             }
307           }
308         }
309         logger.debug("stats data read in {}ms", System.currentTimeMillis() - start);
310       } catch (ExceptionInInitializerError e) {
311         if (e.getCause() != null && e.getCause().getMessage() != null && e.getCause().getMessage()
312             .contains("does not \"opens java.util\" to unnamed module")) {
313           logger.error(
314               "Stats deserialization disabled, use '--add-opens java.base/java.util=ALL-UNNAMED' to start Tomcat to enable again");
315         } else {
316           logger.error("Could not read stats data from '{}' during initialization",
317               file.getAbsolutePath(), e);
318         }
319       } catch (Exception e) {
320         logger.error("Could not read stats data from '{}'", file.getAbsolutePath(), e);
321       }
322     }
323 
324     return stats;
325   }
326 
327   /**
328    * Lock for update.
329    *
330    * @throws InterruptedException the interrupted exception
331    */
332   public void lockForUpdate() throws InterruptedException {
333     lock.lockForUpdate();
334   }
335 
336   /**
337    * Release lock.
338    */
339   public void releaseLock() {
340     lock.releaseUpdateLock();
341   }
342 
343   /**
344    * Reads stats data from file on disk.
345    */
346   @Override
347   public synchronized void afterPropertiesSet() {
348     int index = 0;
349     Map<String, List<XYDataItem>> stats;
350 
351     while (true) {
352       File file = index == 0 ? makeFile() : new File(makeFile().getAbsolutePath() + "." + index);
353       stats = deserialize(file);
354       index += 1;
355       if (stats != null || index >= maxFiles - 1) {
356         break;
357       }
358     }
359 
360     if (stats != null) {
361       statsData = stats;
362     } else {
363       logger.debug("Stats data file not found. Empty file assumed.");
364     }
365 
366   }
367 
368   @Override
369   public void destroy() throws Exception {
370     serialize();
371   }
372 
373   @Override
374   public void setApplicationContext(ApplicationContext applicationContext) {
375     WebApplicationContext wac = (WebApplicationContext) applicationContext;
376     contextTempDir = (File) wac.getServletContext().getAttribute("jakarta.servlet.context.tempdir");
377   }
378 
379 }