1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #define DEBUG false  // STOPSHIP if true
18 #include "Log.h"
19 
20 #include "AnomalyTracker.h"
21 #include "subscriber_util.h"
22 #include "external/Perfetto.h"
23 #include "guardrail/StatsdStats.h"
24 #include "subscriber/IncidentdReporter.h"
25 #include "subscriber/SubscriberReporter.h"
26 
27 #include <inttypes.h>
28 #include <statslog.h>
29 #include <time.h>
30 
31 namespace android {
32 namespace os {
33 namespace statsd {
34 
AnomalyTracker(const Alert & alert,const ConfigKey & configKey)35 AnomalyTracker::AnomalyTracker(const Alert& alert, const ConfigKey& configKey)
36         : mAlert(alert), mConfigKey(configKey), mNumOfPastBuckets(mAlert.num_buckets() - 1) {
37     VLOG("AnomalyTracker() called");
38     if (mAlert.num_buckets() <= 0) {
39         ALOGE("Cannot create AnomalyTracker with %lld buckets", (long long)mAlert.num_buckets());
40         return;
41     }
42     if (!mAlert.has_trigger_if_sum_gt()) {
43         ALOGE("Cannot create AnomalyTracker without threshold");
44         return;
45     }
46     resetStorage();  // initialization
47 }
48 
~AnomalyTracker()49 AnomalyTracker::~AnomalyTracker() {
50     VLOG("~AnomalyTracker() called");
51 }
52 
resetStorage()53 void AnomalyTracker::resetStorage() {
54     VLOG("resetStorage() called.");
55     mPastBuckets.clear();
56     // Excludes the current bucket.
57     mPastBuckets.resize(mNumOfPastBuckets);
58     mSumOverPastBuckets.clear();
59 }
60 
index(int64_t bucketNum) const61 size_t AnomalyTracker::index(int64_t bucketNum) const {
62     if (bucketNum < 0) {
63         ALOGE("index() was passed a negative bucket number (%lld)!", (long long)bucketNum);
64     }
65     return bucketNum % mNumOfPastBuckets;
66 }
67 
advanceMostRecentBucketTo(const int64_t & bucketNum)68 void AnomalyTracker::advanceMostRecentBucketTo(const int64_t& bucketNum) {
69     VLOG("advanceMostRecentBucketTo() called.");
70     if (mNumOfPastBuckets <= 0) {
71         return;
72     }
73     if (bucketNum <= mMostRecentBucketNum) {
74         ALOGW("Cannot advance buckets backwards (bucketNum=%lld but mMostRecentBucketNum=%lld)",
75               (long long)bucketNum, (long long)mMostRecentBucketNum);
76         return;
77     }
78     // If in the future (i.e. buckets are ancient), just empty out all past info.
79     if (bucketNum >= mMostRecentBucketNum + mNumOfPastBuckets) {
80         resetStorage();
81         mMostRecentBucketNum = bucketNum;
82         return;
83     }
84 
85     // Clear out space by emptying out old mPastBuckets[i] values and update mSumOverPastBuckets.
86     for (int64_t i = mMostRecentBucketNum + 1; i <= bucketNum; i++) {
87         const int idx = index(i);
88         subtractBucketFromSum(mPastBuckets[idx]);
89         mPastBuckets[idx] = nullptr;  // release (but not clear) the old bucket.
90     }
91     mMostRecentBucketNum = bucketNum;
92 }
93 
addPastBucket(const MetricDimensionKey & key,const int64_t & bucketValue,const int64_t & bucketNum)94 void AnomalyTracker::addPastBucket(const MetricDimensionKey& key,
95                                    const int64_t& bucketValue,
96                                    const int64_t& bucketNum) {
97     VLOG("addPastBucket(bucketValue) called.");
98     if (mNumOfPastBuckets == 0 ||
99         bucketNum < 0 || bucketNum <= mMostRecentBucketNum - mNumOfPastBuckets) {
100         return;
101     }
102 
103     const int bucketIndex = index(bucketNum);
104     if (bucketNum <= mMostRecentBucketNum && (mPastBuckets[bucketIndex] != nullptr)) {
105         // We need to insert into an already existing past bucket.
106         std::shared_ptr<DimToValMap>& bucket = mPastBuckets[bucketIndex];
107         auto itr = bucket->find(key);
108         if (itr != bucket->end()) {
109             // Old entry already exists; update it.
110             subtractValueFromSum(key, itr->second);
111             itr->second = bucketValue;
112         } else {
113             bucket->insert({key, bucketValue});
114         }
115         mSumOverPastBuckets[key] += bucketValue;
116     } else {
117         // Bucket does not exist yet (in future or was never made), so we must make it.
118         std::shared_ptr<DimToValMap> bucket = std::make_shared<DimToValMap>();
119         bucket->insert({key, bucketValue});
120         addPastBucket(bucket, bucketNum);
121     }
122 }
123 
addPastBucket(std::shared_ptr<DimToValMap> bucket,const int64_t & bucketNum)124 void AnomalyTracker::addPastBucket(std::shared_ptr<DimToValMap> bucket,
125                                    const int64_t& bucketNum) {
126     VLOG("addPastBucket(bucket) called.");
127     if (mNumOfPastBuckets == 0 ||
128             bucketNum < 0 || bucketNum <= mMostRecentBucketNum - mNumOfPastBuckets) {
129         return;
130     }
131 
132     if (bucketNum <= mMostRecentBucketNum) {
133         // We are updating an old bucket, not adding a new one.
134         subtractBucketFromSum(mPastBuckets[index(bucketNum)]);
135     } else {
136         // Clear space for the new bucket to be at bucketNum.
137         advanceMostRecentBucketTo(bucketNum);
138     }
139     mPastBuckets[index(bucketNum)] = bucket;
140     addBucketToSum(bucket);
141 }
142 
subtractBucketFromSum(const shared_ptr<DimToValMap> & bucket)143 void AnomalyTracker::subtractBucketFromSum(const shared_ptr<DimToValMap>& bucket) {
144     if (bucket == nullptr) {
145         return;
146     }
147     for (const auto& keyValuePair : *bucket) {
148         subtractValueFromSum(keyValuePair.first, keyValuePair.second);
149     }
150 }
151 
152 
subtractValueFromSum(const MetricDimensionKey & key,const int64_t & bucketValue)153 void AnomalyTracker::subtractValueFromSum(const MetricDimensionKey& key,
154                                           const int64_t& bucketValue) {
155     auto itr = mSumOverPastBuckets.find(key);
156     if (itr == mSumOverPastBuckets.end()) {
157         return;
158     }
159     itr->second -= bucketValue;
160     if (itr->second == 0) {
161         mSumOverPastBuckets.erase(itr);
162     }
163 }
164 
addBucketToSum(const shared_ptr<DimToValMap> & bucket)165 void AnomalyTracker::addBucketToSum(const shared_ptr<DimToValMap>& bucket) {
166     if (bucket == nullptr) {
167         return;
168     }
169     // For each dimension present in the bucket, add its value to its corresponding sum.
170     for (const auto& keyValuePair : *bucket) {
171         mSumOverPastBuckets[keyValuePair.first] += keyValuePair.second;
172     }
173 }
174 
getPastBucketValue(const MetricDimensionKey & key,const int64_t & bucketNum) const175 int64_t AnomalyTracker::getPastBucketValue(const MetricDimensionKey& key,
176                                            const int64_t& bucketNum) const {
177     if (bucketNum < 0 || mMostRecentBucketNum < 0
178             || bucketNum <= mMostRecentBucketNum - mNumOfPastBuckets
179             || bucketNum > mMostRecentBucketNum) {
180         return 0;
181     }
182 
183     const auto& bucket = mPastBuckets[index(bucketNum)];
184     if (bucket == nullptr) {
185         return 0;
186     }
187     const auto& itr = bucket->find(key);
188     return itr == bucket->end() ? 0 : itr->second;
189 }
190 
getSumOverPastBuckets(const MetricDimensionKey & key) const191 int64_t AnomalyTracker::getSumOverPastBuckets(const MetricDimensionKey& key) const {
192     const auto& itr = mSumOverPastBuckets.find(key);
193     if (itr != mSumOverPastBuckets.end()) {
194         return itr->second;
195     }
196     return 0;
197 }
198 
detectAnomaly(const int64_t & currentBucketNum,const MetricDimensionKey & key,const int64_t & currentBucketValue)199 bool AnomalyTracker::detectAnomaly(const int64_t& currentBucketNum,
200                                    const MetricDimensionKey& key,
201                                    const int64_t& currentBucketValue) {
202 
203     // currentBucketNum should be the next bucket after pastBuckets. If not, advance so that it is.
204     if (currentBucketNum > mMostRecentBucketNum + 1) {
205         advanceMostRecentBucketTo(currentBucketNum - 1);
206     }
207     return mAlert.has_trigger_if_sum_gt() &&
208            getSumOverPastBuckets(key) + currentBucketValue > mAlert.trigger_if_sum_gt();
209 }
210 
declareAnomaly(const int64_t & timestampNs,int64_t metricId,const MetricDimensionKey & key,int64_t metricValue)211 void AnomalyTracker::declareAnomaly(const int64_t& timestampNs, int64_t metricId,
212                                     const MetricDimensionKey& key, int64_t metricValue) {
213     // TODO(b/110563466): Why receive timestamp? RefractoryPeriod should always be based on
214     // real time right now.
215     if (isInRefractoryPeriod(timestampNs, key)) {
216         VLOG("Skipping anomaly declaration since within refractory period");
217         return;
218     }
219     if (mAlert.has_refractory_period_secs()) {
220         mRefractoryPeriodEndsSec[key] = ((timestampNs + NS_PER_SEC - 1) / NS_PER_SEC) // round up
221                                         + mAlert.refractory_period_secs();
222         // TODO(b/110563466): If we had access to the bucket_size_millis, consider
223         // calling resetStorage()
224         // if (mAlert.refractory_period_secs() > mNumOfPastBuckets * bucketSizeNs) {resetStorage();}
225     }
226 
227     if (!mSubscriptions.empty()) {
228         ALOGI("An anomaly (%" PRId64 ") %s has occurred! Informing subscribers.",
229                 mAlert.id(), key.toString().c_str());
230         informSubscribers(key, metricId, metricValue);
231     } else {
232         ALOGI("An anomaly has occurred! (But no subscriber for that alert.)");
233     }
234 
235     StatsdStats::getInstance().noteAnomalyDeclared(mConfigKey, mAlert.id());
236 
237     // TODO(b/110564268): This should also take in the const MetricDimensionKey& key?
238     android::util::stats_write(android::util::ANOMALY_DETECTED, mConfigKey.GetUid(),
239                                mConfigKey.GetId(), mAlert.id());
240 }
241 
detectAndDeclareAnomaly(const int64_t & timestampNs,const int64_t & currBucketNum,int64_t metricId,const MetricDimensionKey & key,const int64_t & currentBucketValue)242 void AnomalyTracker::detectAndDeclareAnomaly(const int64_t& timestampNs,
243                                              const int64_t& currBucketNum, int64_t metricId,
244                                              const MetricDimensionKey& key,
245                                              const int64_t& currentBucketValue) {
246     if (detectAnomaly(currBucketNum, key, currentBucketValue)) {
247         declareAnomaly(timestampNs, metricId, key, currentBucketValue);
248     }
249 }
250 
isInRefractoryPeriod(const int64_t & timestampNs,const MetricDimensionKey & key) const251 bool AnomalyTracker::isInRefractoryPeriod(const int64_t& timestampNs,
252                                           const MetricDimensionKey& key) const {
253     const auto& it = mRefractoryPeriodEndsSec.find(key);
254     if (it != mRefractoryPeriodEndsSec.end()) {
255         return timestampNs < (it->second *  (int64_t)NS_PER_SEC);
256     }
257     return false;
258 }
259 
informSubscribers(const MetricDimensionKey & key,int64_t metric_id,int64_t metricValue)260 void AnomalyTracker::informSubscribers(const MetricDimensionKey& key, int64_t metric_id,
261                                        int64_t metricValue) {
262     triggerSubscribers(mAlert.id(), metric_id, key, metricValue, mConfigKey, mSubscriptions);
263 }
264 
265 }  // namespace statsd
266 }  // namespace os
267 }  // namespace android
268