Release dashboard image at version 2.1.0
[portal/ric-dashboard.git] / dashboard / webapp-backend / src / main / java / org / oransc / ric / portal / dashboard / AppStatsManager.java
1 /*-
2  * ========================LICENSE_START=================================
3  * O-RAN-SC
4  * %%
5  * Copyright (C) 2020 AT&T Intellectual Property
6  * %%
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  * 
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  * 
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ========================LICENSE_END===================================
19  */
20 package org.oransc.ric.portal.dashboard;
21
22 import java.io.File;
23 import java.io.IOException;
24 import java.lang.invoke.MethodHandles;
25 import java.nio.file.Files;
26 import java.util.ArrayList;
27 import java.util.List;
28
29 import org.oransc.ric.portal.dashboard.exception.StatsManagerException;
30 import org.oransc.ric.portal.dashboard.model.AppStats;
31 import org.oransc.ric.portal.dashboard.model.StatsDetailsTransport;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 import com.fasterxml.jackson.core.type.TypeReference;
36 import com.fasterxml.jackson.databind.ObjectMapper;
37
38 /**
39  * Provides simple xApp stats-management services.
40  * 
41  * This first implementation serializes xApp stat details to a file.
42  * 
43  * Migrate to a database someday?
44  */
45
46 public class AppStatsManager {
47
48         private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
49
50         // This default value is only useful for development and testing.
51         public static final String STATS_FILE_PATH = "app-stats.json";
52
53         private final File statsFile;
54         private final List<AppStats> stats;
55         private int appMaxId = -1;
56
57         /**
58          * Development/test-only constructor that uses default file path.
59          * 
60          * @param clear
61          *                  If true, start empty and remove any existing file.
62          * 
63          * @throws IOException
64          *                         On file error
65          */
66         public AppStatsManager(boolean clear) throws IOException {
67                 this(STATS_FILE_PATH);
68                 if (clear) {
69                         logger.debug("ctor: removing file {}", statsFile.getAbsolutePath());
70                         File f = new File(AppStatsManager.STATS_FILE_PATH);
71                         if (f.exists())
72                                 Files.delete(f.toPath());
73                         stats.clear();
74                 }
75         }
76
77         /**
78          * Constructor that accepts a file path
79          * 
80          * @param statsFilePath
81          *                          File path
82          * @throws IOException
83          *                         If file cannot be read
84          */
85         public AppStatsManager(final String statsFilePath) throws IOException {
86                 logger.debug("ctor: statsfile {}", statsFilePath);
87                 if (statsFilePath == null)
88                         throw new IllegalArgumentException("Missing or empty stats file property");
89                 statsFile = new File(statsFilePath);
90                 logger.debug("ctor: managing stats in file {}", statsFile.getAbsolutePath());
91                 if (statsFile.exists()) {
92                         final ObjectMapper mapper = new ObjectMapper();
93                         stats = mapper.readValue(statsFile, new TypeReference<List<AppStats>>() {
94                         });
95                         for (AppStats st : stats) {
96                                 if (st.getStatsDetails().getAppId() > appMaxId)
97                                         appMaxId = st.getStatsDetails().getAppId();
98                         }
99                 } else {
100                         stats = new ArrayList<>();
101                 }
102         }
103
104         /**
105          * Gets the current app metric stats.
106          * 
107          * @return List of App stat objects, possibly empty
108          */
109         public List<AppStats> getStats() {
110                 return this.stats;
111         }
112
113         /**
114          * Gets the current app metric stats by instance key.
115          * 
116          * @param instanceKey
117          *                        Desired instance key
118          * @return List of App stat objects by instance key, possibly empty
119          */
120         public List<AppStats> getStatsByInstance(String instanceKey) {
121                 List<AppStats> statsByInstance = new ArrayList<>();
122                 for (AppStats st : this.stats) {
123                         if (st.getInstanceKey().equals(instanceKey)) {
124                                 logger.debug("getStatsByInstance: match on instance key {}", instanceKey);
125                                 statsByInstance.add(st);
126                         }
127                 }
128                 return statsByInstance;
129         }
130
131         /**
132          * Gets the stats with the specified app Id and instance key
133          * 
134          * @param appId
135          *                        Desired app Id
136          * @param instanceKey
137          *                        Desired instance key
138          * @return Stats object; null if Id is not known
139          */
140         public AppStats getStatsById(String instanceKey, int appId) {
141
142                 for (AppStats st : this.stats) {
143                         if (st.getInstanceKey().equals(instanceKey) && st.getStatsDetails().getAppId() == appId) {
144                                 logger.debug("getStatsById: match on app id {} with instance key {}", appId, instanceKey);
145                                 return st;
146                         }
147                 }
148                 logger.debug("getStatsById: no match on app id with instance key {}{}", appId, instanceKey);
149                 return null;
150
151         }
152
153         private void saveStats() throws IOException {
154                 final ObjectMapper mapper = new ObjectMapper();
155                 mapper.writeValue(statsFile, stats);
156         }
157
158         /*
159          * Allow at most one thread to create a stats at one time. Before creating new
160          * stat, checks for composite key (appname,url) uniqueness for an instance key
161          */
162         public synchronized AppStats createStats(String instanceKey, StatsDetailsTransport statsSetupRequest)
163                         throws StatsManagerException, IOException {
164                 logger.debug("createStats: appId {}, instanceKey {}", statsSetupRequest.getAppId(), instanceKey);
165
166                 for (AppStats st : stats) {
167                         if (st.getInstanceKey().equals(instanceKey)
168                                         && st.getStatsDetails().getAppName().equals(statsSetupRequest.getAppName())
169                                         && st.getStatsDetails().getMetricUrl().equals(statsSetupRequest.getMetricUrl())) {
170                                 // Log the existing object to avoid using tainted (user-supplied) data
171                                 String msg = "App exists with name " + st.getStatsDetails().getAppName() + " and url "
172                                                 + st.getStatsDetails().getMetricUrl() + " on instance key " + st.getInstanceKey();
173                                 logger.warn(msg);
174                                 throw new StatsManagerException(msg);
175                         }
176                 }
177
178                 AppStats newAppStat = null;
179                 // Assigns appId to be 1 more than the largest value stored in memory
180                 appMaxId = appMaxId + 1;
181                 newAppStat = new AppStats(instanceKey,
182                                 new StatsDetailsTransport(appMaxId, statsSetupRequest.getAppName(), statsSetupRequest.getMetricUrl()));
183                 stats.add(newAppStat);
184                 saveStats();
185                 return newAppStat;
186         }
187
188         /*
189          * Allow at most one thread to modify a stats at one time. We still have
190          * last-edit-wins of course.
191          */
192         public synchronized void updateStats(String instanceKey, StatsDetailsTransport statsSetupRequest)
193                         throws StatsManagerException, IOException {
194                 logger.debug("updateStats: appId {}, instanceKey {}", statsSetupRequest.getAppId(), instanceKey);
195                 boolean statsObjectFound = false;
196
197                 for (AppStats st : stats) {
198                         if (st.getInstanceKey().equals(instanceKey)
199                                         && st.getStatsDetails().getAppId() == statsSetupRequest.getAppId()) {
200                                 AppStats newAppStat = new AppStats(instanceKey, statsSetupRequest);
201                                 stats.remove(st);
202                                 stats.add(newAppStat);
203                                 statsObjectFound = true;
204                                 saveStats();
205                                 break;
206                         }
207                 }
208                 if (!statsObjectFound) {
209                         String msg = "Stats to be updated does not exist ";
210                         logger.warn(msg);
211                         throw new StatsManagerException(msg);
212                 }
213         }
214
215         public synchronized AppStats deleteStats(String instanceKey, int appId) throws StatsManagerException, IOException {
216                 logger.debug("deleteStats: appId {}, instanceKey {}", appId, instanceKey);
217                 boolean statsObjectFound = false;
218                 AppStats stat = null;
219                 for (AppStats st : stats) {
220                         if (st.getInstanceKey().equals(instanceKey) && st.getStatsDetails().getAppId() == appId) {
221                                 stat = st;
222                                 stats.remove(stat);
223                                 statsObjectFound = true;
224                                 try {
225                                         saveStats();
226                                         break;
227                                 } catch (Exception e) {
228                                         throw new StatsManagerException(e.toString());
229                                 }
230
231                         }
232                 }
233                 if (!statsObjectFound) {
234                         String msg = "deleteStats: no match on app id {} of instance key {}";
235                         // Replace log pattern-breaking characters
236                         instanceKey = instanceKey.replaceAll("[\n|\r|\t]", "_");
237                         logger.warn(msg, appId, instanceKey);
238                         throw new StatsManagerException(msg);
239                 }
240                 return stat;
241         }
242 }