diff --git a/cmds/statsd/Android.mk b/cmds/statsd/Android.mk index 45ae56f214dda..1b2f9da22ec16 100644 --- a/cmds/statsd/Android.mk +++ b/cmds/statsd/Android.mk @@ -21,6 +21,7 @@ statsd_common_src := \ src/statsd_config.proto \ src/atoms_copy.proto \ src/anomaly/AnomalyMonitor.cpp \ + src/anomaly/AnomalyTracker.cpp \ src/condition/CombinationConditionTracker.cpp \ src/condition/condition_util.cpp \ src/condition/SimpleConditionTracker.cpp \ @@ -39,7 +40,6 @@ statsd_common_src := \ src/matchers/CombinationLogMatchingTracker.cpp \ src/matchers/matcher_util.cpp \ src/matchers/SimpleLogMatchingTracker.cpp \ - src/anomaly/DiscreteAnomalyTracker.cpp \ src/metrics/MetricProducer.cpp \ src/metrics/EventMetricProducer.cpp \ src/metrics/CountMetricProducer.cpp \ @@ -178,4 +178,3 @@ statsd_common_aidl_includes:= statsd_common_c_includes:= include $(BUILD_NATIVE_TEST) - diff --git a/cmds/statsd/src/StatsLogProcessor.cpp b/cmds/statsd/src/StatsLogProcessor.cpp index abd2a35d76dfd..fcb5107c31fcb 100644 --- a/cmds/statsd/src/StatsLogProcessor.cpp +++ b/cmds/statsd/src/StatsLogProcessor.cpp @@ -50,13 +50,24 @@ const int FIELD_ID_UID = 1; const int FIELD_ID_NAME = 2; StatsLogProcessor::StatsLogProcessor(const sp& uidMap, + const sp& anomalyMonitor, const std::function& sendBroadcast) - : mUidMap(uidMap), mSendBroadcast(sendBroadcast) { + : mUidMap(uidMap), mAnomalyMonitor(anomalyMonitor), mSendBroadcast(sendBroadcast) { } StatsLogProcessor::~StatsLogProcessor() { } +void StatsLogProcessor::onAnomalyAlarmFired( + const uint64_t timestampNs, + unordered_set, SpHash> anomalySet) { + for (const auto& anomaly : anomalySet) { + for (const auto& itr : mMetricsManagers) { + itr.second->onAnomalyAlarmFired(timestampNs, anomaly); + } + } +} + // TODO: what if statsd service restarts? How do we know what logs are already processed before? void StatsLogProcessor::OnLogEvent(const LogEvent& msg) { // pass the event to metrics managers. @@ -93,6 +104,7 @@ void StatsLogProcessor::OnConfigUpdated(const ConfigKey& key, const StatsdConfig unique_ptr newMetricsManager = std::make_unique(config); if (newMetricsManager->isConfigValid()) { mUidMap->OnConfigUpdated(key); + newMetricsManager->setAnomalyMonitor(mAnomalyMonitor); mMetricsManagers[key] = std::move(newMetricsManager); // Why doesn't this work? mMetricsManagers.insert({key, std::move(newMetricsManager)}); ALOGD("StatsdConfig valid"); diff --git a/cmds/statsd/src/StatsLogProcessor.h b/cmds/statsd/src/StatsLogProcessor.h index 2091774b108ee..510dc5145dfe3 100644 --- a/cmds/statsd/src/StatsLogProcessor.h +++ b/cmds/statsd/src/StatsLogProcessor.h @@ -32,7 +32,7 @@ namespace statsd { class StatsLogProcessor : public ConfigListener { public: - StatsLogProcessor(const sp& uidMap, + StatsLogProcessor(const sp& uidMap, const sp& anomalyMonitor, const std::function& sendBroadcast); virtual ~StatsLogProcessor(); @@ -42,8 +42,11 @@ public: void OnConfigRemoved(const ConfigKey& key); size_t GetMetricsSize(const ConfigKey& key); - + void onDumpReport(const ConfigKey& key, vector* outData); + void onAnomalyAlarmFired( + const uint64_t timestampNs, + unordered_set, SpHash> anomalySet); private: mutable mutex mBroadcastTimesMutex; @@ -54,6 +57,8 @@ private: sp mUidMap; // Reference to the UidMap to lookup app name and version for each uid. + sp mAnomalyMonitor; + /* Max *serialized* size of the logs kept in memory before flushing through binder call. Proto lite does not implement the SpaceUsed() function which gives the in memory byte size. So we cap memory usage by limiting the serialized size. Note that protobuf's in memory size diff --git a/cmds/statsd/src/StatsService.cpp b/cmds/statsd/src/StatsService.cpp index 31d32b43d1c03..1a056df8e367f 100644 --- a/cmds/statsd/src/StatsService.cpp +++ b/cmds/statsd/src/StatsService.cpp @@ -73,7 +73,7 @@ StatsService::StatsService(const sp& handlerLooper) { mUidMap = new UidMap(); mConfigManager = new ConfigManager(); - mProcessor = new StatsLogProcessor(mUidMap, [this](const ConfigKey& key) { + mProcessor = new StatsLogProcessor(mUidMap, mAnomalyMonitor, [this](const ConfigKey& key) { auto sc = getStatsCompanionService(); auto receiver = mConfigManager->GetConfigReceiver(key); if (sc == nullptr) { @@ -554,7 +554,10 @@ Status StatsService::informAnomalyAlarmFired() { if (DEBUG) ALOGD("StatsService::informAnomalyAlarmFired succeeded"); // TODO: check through all counters/timers and see if an anomaly has indeed occurred. - + uint64_t currentTimeNs = time(nullptr) * NS_PER_SEC; + std::unordered_set, SpHash> anomalySet = + mAnomalyMonitor->onAlarmFired(currentTimeNs); + mProcessor->onAnomalyAlarmFired(currentTimeNs, anomalySet); return Status::ok(); } diff --git a/cmds/statsd/src/anomaly/AnomalyMonitor.cpp b/cmds/statsd/src/anomaly/AnomalyMonitor.cpp index 7a464104fb09e..da52a9d664944 100644 --- a/cmds/statsd/src/anomaly/AnomalyMonitor.cpp +++ b/cmds/statsd/src/anomaly/AnomalyMonitor.cpp @@ -129,6 +129,11 @@ int64_t AnomalyMonitor::secToMs(uint32_t timeSec) { return ((int64_t)timeSec) * 1000; } +unordered_set, SpHash> AnomalyMonitor::onAlarmFired( + uint64_t timestampNs) { + return popSoonerThan(static_cast(timestampNs)); +} + } // namespace statsd } // namespace os } // namespace android diff --git a/cmds/statsd/src/anomaly/AnomalyMonitor.h b/cmds/statsd/src/anomaly/AnomalyMonitor.h index d9207e99dd6f2..0bd5055631b60 100644 --- a/cmds/statsd/src/anomaly/AnomalyMonitor.h +++ b/cmds/statsd/src/anomaly/AnomalyMonitor.h @@ -14,8 +14,7 @@ * limitations under the License. */ -#ifndef ANOMALY_MONITOR_H -#define ANOMALY_MONITOR_H +#pragma once #include "anomaly/indexed_priority_queue.h" @@ -23,6 +22,8 @@ #include #include +#include +#include #include #include @@ -114,6 +115,8 @@ public: return mRegisteredAlarmTimeSec; } + unordered_set, SpHash> onAlarmFired(uint64_t timestampNs); + private: std::mutex mLock; @@ -154,5 +157,3 @@ private: } // namespace statsd } // namespace os } // namespace android - -#endif // ANOMALY_MONITOR_H diff --git a/cmds/statsd/src/anomaly/AnomalyTracker.cpp b/cmds/statsd/src/anomaly/AnomalyTracker.cpp new file mode 100644 index 0000000000000..0904a04987e70 --- /dev/null +++ b/cmds/statsd/src/anomaly/AnomalyTracker.cpp @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define DEBUG true // STOPSHIP if true +#include "Log.h" + +#include "AnomalyTracker.h" + +#include + +namespace android { +namespace os { +namespace statsd { + +AnomalyTracker::AnomalyTracker(const Alert& alert, const int64_t& bucketSizeNs) + : mAlert(alert), + mBucketSizeNs(bucketSizeNs), + mNumOfPastPackets(mAlert.number_of_buckets() - 1) { + VLOG("AnomalyTracker() called"); + if (mAlert.number_of_buckets() <= 0) { + ALOGE("Cannot create DiscreteAnomalyTracker with %lld buckets", + (long long)mAlert.number_of_buckets()); + return; + } + if (mBucketSizeNs <= 0) { + ALOGE("Cannot create DiscreteAnomalyTracker with bucket size %lld ", + (long long)mBucketSizeNs); + return; + } + if (!mAlert.has_trigger_if_sum_gt()) { + ALOGE("Cannot create DiscreteAnomalyTracker without threshold"); + return; + } + reset(); // initialization +} + +AnomalyTracker::~AnomalyTracker() { + VLOG("~AnomalyTracker() called"); + stopAllAlarms(); +} + +void AnomalyTracker::reset() { + VLOG("reset() called."); + stopAllAlarms(); + mPastBuckets.clear(); + // Excludes the current bucket. + mPastBuckets.resize(mNumOfPastPackets); + mSumOverPastBuckets.clear(); + mMostRecentBucketNum = -1; + mLastAlarmTimestampNs = -1; +} + +size_t AnomalyTracker::index(int64_t bucketNum) const { + return bucketNum % mNumOfPastPackets; +} + +void AnomalyTracker::flushPastBuckets(const int64_t& latestPastBucketNum) { + VLOG("addPastBucket() called."); + if (latestPastBucketNum <= mMostRecentBucketNum - mNumOfPastPackets) { + ALOGE("Cannot add a past bucket %lld units in past", (long long)latestPastBucketNum); + return; + } + + // The past packets are ancient. Empty out old mPastBuckets[i] values and reset + // mSumOverPastBuckets. + if (latestPastBucketNum - mMostRecentBucketNum >= mNumOfPastPackets) { + mPastBuckets.clear(); + mPastBuckets.resize(mNumOfPastPackets); + mSumOverPastBuckets.clear(); + } else { + for (int64_t i = std::max(0LL, (long long)(mMostRecentBucketNum - mNumOfPastPackets + 1)); + i <= latestPastBucketNum - mNumOfPastPackets; i++) { + const int idx = index(i); + subtractBucketFromSum(mPastBuckets[idx]); + mPastBuckets[idx] = nullptr; // release (but not clear) the old bucket. + } + } + + // It is an update operation. + if (latestPastBucketNum <= mMostRecentBucketNum && + latestPastBucketNum > mMostRecentBucketNum - mNumOfPastPackets) { + subtractBucketFromSum(mPastBuckets[index(latestPastBucketNum)]); + } +} + +void AnomalyTracker::addPastBucket(const HashableDimensionKey& key, const int64_t& bucketValue, + const int64_t& bucketNum) { + flushPastBuckets(bucketNum); + + auto& bucket = mPastBuckets[index(bucketNum)]; + if (bucket == nullptr) { + bucket = std::make_shared(); + } + bucket->insert({key, bucketValue}); + addBucketToSum(bucket); + mMostRecentBucketNum = std::max(mMostRecentBucketNum, bucketNum); +} + +void AnomalyTracker::addPastBucket(std::shared_ptr bucketValues, + const int64_t& bucketNum) { + VLOG("addPastBucket() called."); + flushPastBuckets(bucketNum); + // Replace the oldest bucket with the new bucket we are adding. + mPastBuckets[index(bucketNum)] = bucketValues; + addBucketToSum(bucketValues); + mMostRecentBucketNum = std::max(mMostRecentBucketNum, bucketNum); +} + +void AnomalyTracker::subtractBucketFromSum(const shared_ptr& bucket) { + if (bucket == nullptr) { + return; + } + // For each dimension present in the bucket, subtract its value from its corresponding sum. + for (const auto& keyValuePair : *bucket) { + auto itr = mSumOverPastBuckets.find(keyValuePair.first); + if (itr == mSumOverPastBuckets.end()) { + continue; + } + itr->second -= keyValuePair.second; + // TODO: No need to look up the object twice like this. Use a var. + if (itr->second == 0) { + mSumOverPastBuckets.erase(itr); + } + } +} + +void AnomalyTracker::addBucketToSum(const shared_ptr& bucket) { + if (bucket == nullptr) { + return; + } + // For each dimension present in the bucket, add its value to its corresponding sum. + for (const auto& keyValuePair : *bucket) { + mSumOverPastBuckets[keyValuePair.first] += keyValuePair.second; + } +} + +int64_t AnomalyTracker::getPastBucketValue(const HashableDimensionKey& key, + const int64_t& bucketNum) const { + const auto& bucket = mPastBuckets[index(bucketNum)]; + if (bucket == nullptr) { + return 0; + } + const auto& itr = bucket->find(key); + return itr == bucket->end() ? 0 : itr->second; +} + +int64_t AnomalyTracker::getSumOverPastBuckets(const HashableDimensionKey& key) const { + const auto& itr = mSumOverPastBuckets.find(key); + if (itr != mSumOverPastBuckets.end()) { + return itr->second; + } + return 0; +} + +bool AnomalyTracker::detectAnomaly(const int64_t& currentBucketNum, + const DimToValMap& currentBucket) { + if (currentBucketNum > mMostRecentBucketNum + 1) { + addPastBucket(nullptr, currentBucketNum - 1); + } + for (auto itr = currentBucket.begin(); itr != currentBucket.end(); itr++) { + if (itr->second + getSumOverPastBuckets(itr->first) > mAlert.trigger_if_sum_gt()) { + return true; + } + } + // In theory, we also need to check the dimsions not in the current bucket. In single-thread + // mode, usually we could avoid the following loops. + for (auto itr = mSumOverPastBuckets.begin(); itr != mSumOverPastBuckets.end(); itr++) { + if (itr->second > mAlert.trigger_if_sum_gt()) { + return true; + } + } + return false; +} + +bool AnomalyTracker::detectAnomaly(const int64_t& currentBucketNum, const HashableDimensionKey& key, + const int64_t& currentBucketValue) { + if (currentBucketNum > mMostRecentBucketNum + 1) { + addPastBucket(key, 0, currentBucketNum - 1); + } + return getSumOverPastBuckets(key) + currentBucketValue > mAlert.trigger_if_sum_gt(); +} + +void AnomalyTracker::declareAnomaly(const uint64_t& timestamp) { + if (mLastAlarmTimestampNs >= 0 && + timestamp - mLastAlarmTimestampNs <= mAlert.refractory_period_secs() * NS_PER_SEC) { + VLOG("Skipping anomaly check since within refractory period"); + return; + } + // TODO(guardrail): Consider guarding against too short refractory periods. + mLastAlarmTimestampNs = timestamp; + + if (mAlert.has_incidentd_details()) { + // TODO: Can construct a name based on the criteria (and/or relay the criteria). + ALOGW("An anomaly (nameless) has occurred! Informing incidentd."); + // TODO: Send incidentd_details.name and incidentd_details.incidentd_sections to incidentd + } else { + ALOGW("An anomaly has occurred! (But informing incidentd not requested.)"); + } +} + +void AnomalyTracker::declareAnomalyIfAlarmExpired(const HashableDimensionKey& dimensionKey, + const uint64_t& timestamp) { + auto itr = mAlarms.find(dimensionKey); + if (itr == mAlarms.end()) { + return; + } + + if (itr->second != nullptr && + static_cast(timestamp / NS_PER_SEC) >= itr->second->timestampSec) { + declareAnomaly(timestamp); + stopAlarm(dimensionKey); + } +} + +void AnomalyTracker::detectAndDeclareAnomaly(const uint64_t& timestamp, + const int64_t& currBucketNum, + const HashableDimensionKey& key, + const int64_t& currentBucketValue) { + if (detectAnomaly(currBucketNum, key, currentBucketValue)) { + declareAnomaly(timestamp); + } +} + +void AnomalyTracker::detectAndDeclareAnomaly(const uint64_t& timestamp, + const int64_t& currBucketNum, + const DimToValMap& currentBucket) { + if (detectAnomaly(currBucketNum, currentBucket)) { + declareAnomaly(timestamp); + } +} + +void AnomalyTracker::startAlarm(const HashableDimensionKey& dimensionKey, + const uint64_t& timestamp) { + sp alarm = new AnomalyAlarm{static_cast(timestamp / NS_PER_SEC)}; + mAlarms.insert({dimensionKey, alarm}); + if (mAnomalyMonitor != nullptr) { + mAnomalyMonitor->add(alarm); + } +} + +void AnomalyTracker::stopAlarm(const HashableDimensionKey& dimensionKey) { + auto itr = mAlarms.find(dimensionKey); + if (itr != mAlarms.end()) { + mAlarms.erase(dimensionKey); + } + if (mAnomalyMonitor != nullptr) { + mAnomalyMonitor->remove(itr->second); + } +} + +void AnomalyTracker::stopAllAlarms() { + std::set keys; + for (auto itr = mAlarms.begin(); itr != mAlarms.end(); ++itr) { + keys.insert(itr->first); + } + for (auto key : keys) { + stopAlarm(key); + } +} + +} // namespace statsd +} // namespace os +} // namespace android diff --git a/cmds/statsd/src/anomaly/AnomalyTracker.h b/cmds/statsd/src/anomaly/AnomalyTracker.h new file mode 100644 index 0000000000000..ce6c99518cda9 --- /dev/null +++ b/cmds/statsd/src/anomaly/AnomalyTracker.h @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include "AnomalyMonitor.h" +#include "frameworks/base/cmds/statsd/src/statsd_config.pb.h" // Alert +#include "stats_util.h" // HashableDimensionKey and DimToValMap + +#include // unique_ptr +#include +#include + +namespace android { +namespace os { +namespace statsd { + +using std::unordered_map; +using std::shared_ptr; + +// This anomaly track assmues that all values are non-negative. +class AnomalyTracker : public virtual RefBase { +public: + AnomalyTracker(const Alert& alert, const int64_t& bucketSizeNs); + + virtual ~AnomalyTracker(); + + // Adds a bucket. + // Bucket index starts from 0. + void addPastBucket(std::shared_ptr bucketValues, const int64_t& bucketNum); + void addPastBucket(const HashableDimensionKey& key, const int64_t& bucketValue, + const int64_t& bucketNum); + + // Returns true if detected anomaly for the existing buckets on one or more dimension keys. + bool detectAnomaly(const int64_t& currBucketNum, const DimToValMap& currentBucket); + bool detectAnomaly(const int64_t& currBucketNum, const HashableDimensionKey& key, + const int64_t& currentBucketValue); + + // Informs incidentd about the detected alert. + void declareAnomaly(const uint64_t& timestamp); + + // Detects the alert and informs the incidentd when applicable. + void detectAndDeclareAnomaly(const uint64_t& timestamp, const int64_t& currBucketNum, + const DimToValMap& currentBucket); + void detectAndDeclareAnomaly(const uint64_t& timestamp, const int64_t& currBucketNum, + const HashableDimensionKey& key, + const int64_t& currentBucketValue); + + // Starts the alarm at the given timestamp. + void startAlarm(const HashableDimensionKey& dimensionKey, const uint64_t& eventTime); + // Stops the alarm. + void stopAlarm(const HashableDimensionKey& dimensionKey); + + // Stop all the alarms owned by this tracker. + void stopAllAlarms(); + + // Init the anmaly monitor which is shared across anomaly trackers. + inline void setAnomalyMonitor(const sp& anomalyMonitor) { + mAnomalyMonitor = anomalyMonitor; + } + + // Declares the anomaly when the alarm expired given the current timestamp. + void declareAnomalyIfAlarmExpired(const HashableDimensionKey& dimensionKey, + const uint64_t& timestamp); + + // Helper function to return the sum value of past buckets at given dimension. + int64_t getSumOverPastBuckets(const HashableDimensionKey& key) const; + + // Helper function to return the value for a past bucket. + int64_t getPastBucketValue(const HashableDimensionKey& key, const int64_t& bucketNum) const; + + // Returns the anomaly threshold. + inline int64_t getAnomalyThreshold() const { + return mAlert.trigger_if_sum_gt(); + } + + // Helper function to return the last alarm timestamp. + inline int64_t getLastAlarmTimestampNs() const { + return mLastAlarmTimestampNs; + } + + inline int getNumOfPastPackets() const { + return mNumOfPastPackets; + } + +protected: + void flushPastBuckets(const int64_t& currBucketNum); + // statsd_config.proto Alert message that defines this tracker. + const Alert mAlert; + + // Bucket duration in ns. + int64_t mBucketSizeNs = 0; + + // The number of past packets to track in the anomaly detection. + int mNumOfPastPackets = 0; + + // The alarms owned by this tracker. The alarm monitor also shares the alarm pointers when they + // are still active. + std::unordered_map> mAlarms; + + // Anomaly alarm monitor. + sp mAnomalyMonitor; + + // The exisiting bucket list. + std::vector> mPastBuckets; + + // Sum over all existing buckets cached in mPastBuckets. + DimToValMap mSumOverPastBuckets; + + // The bucket number of the last added bucket. + int64_t mMostRecentBucketNum = -1; + + // The timestamp when the last anomaly was declared. + int64_t mLastAlarmTimestampNs = -1; + + // Add the information in the given bucket to mSumOverPastBuckets. + void addBucketToSum(const shared_ptr& bucket); + + // Subtract the information in the given bucket from mSumOverPastBuckets + // and remove any items with value 0. + void subtractBucketFromSum(const shared_ptr& bucket); + + // Calculates the corresponding bucket index within the circular array. + size_t index(int64_t bucketNum) const; + + // Resets all data. For use when all the data gets stale. + void reset(); + + FRIEND_TEST(AnomalyTrackerTest, TestConsecutiveBuckets); + FRIEND_TEST(AnomalyTrackerTest, TestSparseBuckets); + FRIEND_TEST(GaugeMetricProducerTest, TestAnomalyDetection); + FRIEND_TEST(CountMetricProducerTest, TestAnomalyDetection); + FRIEND_TEST(OringDurationTrackerTest, TestPredictAnomalyTimestamp); + FRIEND_TEST(OringDurationTrackerTest, TestAnomalyDetection); + FRIEND_TEST(MaxDurationTrackerTest, TestAnomalyDetection); + FRIEND_TEST(MaxDurationTrackerTest, TestAnomalyDetection); + FRIEND_TEST(OringDurationTrackerTest, TestAnomalyDetection); +}; + +} // namespace statsd +} // namespace os +} // namespace android diff --git a/cmds/statsd/src/anomaly/DiscreteAnomalyTracker.cpp b/cmds/statsd/src/anomaly/DiscreteAnomalyTracker.cpp deleted file mode 100644 index 64921779acdcc..0000000000000 --- a/cmds/statsd/src/anomaly/DiscreteAnomalyTracker.cpp +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#define DEBUG true // STOPSHIP if true -#include "Log.h" - -#include "DiscreteAnomalyTracker.h" - -#include - -namespace android { -namespace os { -namespace statsd { - -DiscreteAnomalyTracker::DiscreteAnomalyTracker(const Alert& alert) : mAlert(alert) { - VLOG("DiscreteAnomalyTracker() called"); - if (mAlert.number_of_buckets() <= 0) { - ALOGE("Cannot create DiscreteAnomalyTracker with %lld buckets", - (long long)mAlert.number_of_buckets()); - return; - } - mPastBuckets.resize(mAlert.number_of_buckets()); - reset(); // initialization -} - -DiscreteAnomalyTracker::~DiscreteAnomalyTracker() { - VLOG("~DiscreteAnomalyTracker() called"); -} - -void DiscreteAnomalyTracker::reset() { - VLOG("reset() called."); - mPastBuckets.clear(); - mPastBuckets.resize(mAlert.number_of_buckets()); - mSumOverPastBuckets.clear(); - mCurrentBucketIndex = -1; - mLastAlarmAtBucketIndex = -1; - mAnomalyDeclared = 0; -} - -size_t DiscreteAnomalyTracker::index(int64_t bucketNum) { - return bucketNum % mAlert.number_of_buckets(); -} - -void DiscreteAnomalyTracker::addOrUpdateBucket(std::shared_ptr BucketValues, - int64_t bucketIndex) { - VLOG("addPastBucket() called."); - if (bucketIndex <= mCurrentBucketIndex - mAlert.number_of_buckets()) { - ALOGE("Cannot add a past bucket %lld units in past", (long long)bucketIndex); - return; - } - - // Empty out old mPastBuckets[i] values and update mSumOverPastBuckets. - if (bucketIndex - mCurrentBucketIndex >= mAlert.number_of_buckets()) { - mPastBuckets.clear(); - mPastBuckets.resize(mAlert.number_of_buckets()); - mSumOverPastBuckets.clear(); - } else { - for (int64_t i = std::max( - 0LL, (long long)(mCurrentBucketIndex - mAlert.number_of_buckets() + 1)); - i < bucketIndex - mAlert.number_of_buckets(); i++) { - const int idx = index(i); - subtractBucketFromSum(mPastBuckets[idx]); - mPastBuckets[idx] = nullptr; // release (but not clear) the old bucket. - } - } - subtractBucketFromSum(mPastBuckets[index(bucketIndex)]); - mPastBuckets[index(bucketIndex)] = nullptr; // release (but not clear) the old bucket. - - // Replace the oldest bucket with the new bucket we are adding. - mPastBuckets[index(bucketIndex)] = BucketValues; - addBucketToSum(BucketValues); - - mCurrentBucketIndex = std::max(mCurrentBucketIndex, bucketIndex); -} - -void DiscreteAnomalyTracker::subtractBucketFromSum(const shared_ptr& bucket) { - if (bucket == nullptr) { - return; - } - // For each dimension present in the bucket, subtract its value from its corresponding sum. - for (const auto& keyValuePair : *bucket) { - auto itr = mSumOverPastBuckets.find(keyValuePair.first); - if (itr == mSumOverPastBuckets.end()) { - continue; - } - itr->second -= keyValuePair.second; - // TODO: No need to look up the object twice like this. Use a var. - if (itr->second == 0) { - mSumOverPastBuckets.erase(itr); - } - } -} - -void DiscreteAnomalyTracker::addBucketToSum(const shared_ptr& bucket) { - if (bucket == nullptr) { - return; - } - // For each dimension present in the bucket, add its value to its corresponding sum. - for (const auto& keyValuePair : *bucket) { - mSumOverPastBuckets[keyValuePair.first] += keyValuePair.second; - } -} - -bool DiscreteAnomalyTracker::detectAnomaly() { - for (auto itr = mSumOverPastBuckets.begin(); itr != mSumOverPastBuckets.end(); itr++) { - if (mAlert.has_trigger_if_sum_gt() && itr->second > mAlert.trigger_if_sum_gt()) { - return true; - } - } - return false; -} - -void DiscreteAnomalyTracker::declareAndDeclareAnomaly() { - if (detectAnomaly()) { - declareAnomaly(); - } -} - -void DiscreteAnomalyTracker::declareAnomaly() { - if (mLastAlarmAtBucketIndex >= 0 && mCurrentBucketIndex - mLastAlarmAtBucketIndex <= - (long long)mAlert.refractory_period_in_buckets()) { - VLOG("Skipping anomaly check since within refractory period"); - return; - } - mAnomalyDeclared++; - // TODO(guardrail): Consider guarding against too short refractory periods. - mLastAlarmAtBucketIndex = mCurrentBucketIndex; - - if (mAlert.has_incidentd_details()) { - // TODO: Can construct a name based on the criteria (and/or relay the criteria). - ALOGW("An anomaly (nameless) has occurred! Informing incidentd."); - // TODO: Send incidentd_details.name and incidentd_details.incidentd_sections to incidentd - } else { - ALOGW("An anomaly has occurred! (But informing incidentd not requested.)"); - } -} - -} // namespace statsd -} // namespace os -} // namespace android diff --git a/cmds/statsd/src/anomaly/DiscreteAnomalyTracker.h b/cmds/statsd/src/anomaly/DiscreteAnomalyTracker.h deleted file mode 100644 index ed7d5d715a6d4..0000000000000 --- a/cmds/statsd/src/anomaly/DiscreteAnomalyTracker.h +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include -#include "frameworks/base/cmds/statsd/src/statsd_config.pb.h" // Alert -#include "stats_util.h" // HashableDimensionKey and DimToValMap - -#include // unique_ptr -#include - -namespace android { -namespace os { -namespace statsd { - -using std::unordered_map; -using std::shared_ptr; - -// This anomaly track assmues that all values are non-negative. -class DiscreteAnomalyTracker { - public: - DiscreteAnomalyTracker(const Alert& alert); - - virtual ~DiscreteAnomalyTracker(); - - // Adds a new bucket or updates an existing bucket. - // Bucket index starts from 0. - void addOrUpdateBucket(std::shared_ptr BucketValues, int64_t bucketIndex); - - // Returns true if detected anomaly for the existing buckets on one or more dimension keys. - bool detectAnomaly(); - - // Informs incidentd about the detected alert. - void declareAnomaly(); - - // Detects the alert and informs the incidentd when applicable. - void declareAndDeclareAnomaly(); - -private: - // statsd_config.proto Alert message that defines this tracker. - const Alert mAlert; - - // The exisiting bucket list. - std::vector> mPastBuckets; - - // Sum over all existing buckets cached in mPastBuckets. - DimToValMap mSumOverPastBuckets; - - // Current bucket index of the current anomaly detection window. Bucket index starts from 0. - int64_t mCurrentBucketIndex = -1; - - // The bucket index when the last anomaly was declared. - int64_t mLastAlarmAtBucketIndex = -1; - - // The total number of declared anomalies. - int64_t mAnomalyDeclared = 0; - - // Add the information in the given bucket to mSumOverPastBuckets. - void addBucketToSum(const shared_ptr& bucket); - - // Subtract the information in the given bucket from mSumOverPastBuckets - // and remove any items with value 0. - void subtractBucketFromSum(const shared_ptr& bucket); - - // Calculates the corresponding bucket index within the circular array. - size_t index(int64_t bucketNum); - - // Resets all data. For use when all the data gets stale. - void reset(); - - FRIEND_TEST(AnomalyTrackerTest, TestConsecutiveBuckets); - FRIEND_TEST(AnomalyTrackerTest, TestSparseBuckets); -}; - -} // namespace statsd -} // namespace os -} // namespace android diff --git a/cmds/statsd/src/anomaly/indexed_priority_queue.h b/cmds/statsd/src/anomaly/indexed_priority_queue.h index 1a2e9c2a41ed1..4982d4b90b7df 100644 --- a/cmds/statsd/src/anomaly/indexed_priority_queue.h +++ b/cmds/statsd/src/anomaly/indexed_priority_queue.h @@ -16,8 +16,6 @@ #pragma once -#include "Log.h" - #include #include #include @@ -28,7 +26,7 @@ namespace android { namespace os { namespace statsd { -/** Defines a hash function for sp, returning the hash of the underlying pointer. */ +/** Defines a hash function for sp, returning the hash of the underlying pointer. */ template struct SpHash { size_t operator()(const sp& k) const { @@ -39,7 +37,7 @@ struct SpHash { /** * Min priority queue for generic type AA. * Unlike a regular priority queue, this class is also capable of removing interior elements. - * @tparam Comparator must implement [bool operator()(sp a, sp b)], returning + * @tparam Comparator must implement [bool operator()(sp< AA> a, sp< AA> b)], returning * whether a should be closer to the top of the queue than b. */ template @@ -104,7 +102,6 @@ void indexed_priority_queue::remove(sp a) { if (!contains(a)) return; size_t idx = indices[a]; if (idx >= pq.size()) { - ALOGE("indexed_priority_queue: Invalid index in map of indices."); return; } if (idx == size()) { // if a is the last element, i.e. at index idx == size() == (pq.size()-1) @@ -193,7 +190,6 @@ void indexed_priority_queue::sift_down(size_t idx) { template bool indexed_priority_queue::higher(size_t idx1, size_t idx2) const { if (!(0u < idx1 && idx1 < pq.size() && 0u < idx2 && idx2 < pq.size())) { - ALOGE("indexed_priority_queue: Attempting to access invalid index"); return false; // got to do something. } return Comparator()(pq[idx1], pq[idx2]); @@ -208,7 +204,6 @@ bool indexed_priority_queue::contains(sp a) const { template void indexed_priority_queue::swap_indices(size_t i, size_t j) { if (!(0u < i && i < pq.size() && 0u < j && j < pq.size())) { - ALOGE("indexed_priority_queue: Attempting to swap invalid index"); return; } sp val_i = pq[i]; diff --git a/cmds/statsd/src/metrics/CountMetricProducer.cpp b/cmds/statsd/src/metrics/CountMetricProducer.cpp index f9da68e8e5c52..d47bd4fe9e44f 100644 --- a/cmds/statsd/src/metrics/CountMetricProducer.cpp +++ b/cmds/statsd/src/metrics/CountMetricProducer.cpp @@ -17,7 +17,6 @@ #define DEBUG true // STOPSHIP if true #include "Log.h" -#include "../anomaly/DiscreteAnomalyTracker.h" #include "CountMetricProducer.h" #include "stats_util.h" @@ -114,7 +113,7 @@ std::unique_ptr> CountMetricProducer::onDumpReport() { // Dump current bucket if it's stale. // If current bucket is still on-going, don't force dump current bucket. // In finish(), We can force dump current bucket. - flushCounterIfNeeded(endTime); + flushIfNeeded(endTime); VLOG("metric %s dump report now...", mMetric.name().c_str()); for (const auto& counter : mPastBuckets) { @@ -170,7 +169,6 @@ std::unique_ptr> CountMetricProducer::onDumpReport() { startNewProtoOutputStream(endTime); mPastBuckets.clear(); - mByteSize = 0; return buffer; @@ -188,7 +186,7 @@ void CountMetricProducer::onMatchedLogEventInternal( const LogEvent& event, bool scheduledPull) { uint64_t eventTimeNs = event.GetTimestampNs(); - flushCounterIfNeeded(eventTimeNs); + flushIfNeeded(eventTimeNs); if (condition == false) { return; @@ -205,41 +203,41 @@ void CountMetricProducer::onMatchedLogEventInternal( count++; } - VLOG("metric %s %s->%d", mMetric.name().c_str(), eventKey.c_str(), - (*mCurrentSlicedCounter)[eventKey]); + for (auto& tracker : mAnomalyTrackers) { + tracker->detectAndDeclareAnomaly(eventTimeNs, mCurrentBucketNum, eventKey, + mCurrentSlicedCounter->find(eventKey)->second); + } + + VLOG("metric %s %s->%lld", mMetric.name().c_str(), eventKey.c_str(), + (long long)(*mCurrentSlicedCounter)[eventKey]); } // When a new matched event comes in, we check if event falls into the current // bucket. If not, flush the old counter to past buckets and initialize the new bucket. -void CountMetricProducer::flushCounterIfNeeded(const uint64_t eventTimeNs) { - if (mCurrentBucketStartTimeNs + mBucketSizeNs > eventTimeNs) { +void CountMetricProducer::flushIfNeeded(const uint64_t eventTimeNs) { + if (eventTimeNs < mCurrentBucketStartTimeNs + mBucketSizeNs) { return; } - // adjust the bucket start time - // TODO: This (and addPastBucket to which it goes) doesn't really need to be an int64. - uint64_t numBucketsForward = (eventTimeNs - mCurrentBucketStartTimeNs) / mBucketSizeNs; - CountBucket info; info.mBucketStartNs = mCurrentBucketStartTimeNs; info.mBucketEndNs = mCurrentBucketStartTimeNs + mBucketSizeNs; + info.mBucketNum = mCurrentBucketNum; for (const auto& counter : *mCurrentSlicedCounter) { info.mCount = counter.second; auto& bucketList = mPastBuckets[counter.first]; bucketList.push_back(info); - VLOG("metric %s, dump key value: %s -> %d", mMetric.name().c_str(), counter.first.c_str(), - counter.second); - mByteSize += sizeof(info); + VLOG("metric %s, dump key value: %s -> %lld", mMetric.name().c_str(), counter.first.c_str(), + (long long)counter.second); } for (auto& tracker : mAnomalyTrackers) { - tracker->addOrUpdateBucket(mCurrentSlicedCounter, mCurrentBucketNum); - tracker->declareAndDeclareAnomaly(); + tracker->addPastBucket(mCurrentSlicedCounter, mCurrentBucketNum); } // Reset counters (do not clear, since the old one is still referenced in mAnomalyTrackers). mCurrentSlicedCounter = std::make_shared(); - + uint64_t numBucketsForward = (eventTimeNs - mCurrentBucketStartTimeNs) / mBucketSizeNs; mCurrentBucketStartTimeNs = mCurrentBucketStartTimeNs + numBucketsForward * mBucketSizeNs; mCurrentBucketNum += numBucketsForward; VLOG("metric %s: new bucket start time: %lld", mMetric.name().c_str(), @@ -250,7 +248,11 @@ void CountMetricProducer::flushCounterIfNeeded(const uint64_t eventTimeNs) { // greater than actual data size as it contains each dimension of // CountMetricData is duplicated. size_t CountMetricProducer::byteSize() { - return mByteSize; + size_t totalSize = 0; + for (const auto& pair : mPastBuckets) { + totalSize += pair.second.size() * kBucketSize; + } + return totalSize; } } // namespace statsd diff --git a/cmds/statsd/src/metrics/CountMetricProducer.h b/cmds/statsd/src/metrics/CountMetricProducer.h index b7e480c4e78d1..b3f8ee37379ea 100644 --- a/cmds/statsd/src/metrics/CountMetricProducer.h +++ b/cmds/statsd/src/metrics/CountMetricProducer.h @@ -21,9 +21,9 @@ #include #include +#include "../anomaly/AnomalyTracker.h" #include "../condition/ConditionTracker.h" #include "../matchers/matcher_util.h" -#include "../anomaly/DiscreteAnomalyTracker.h" #include "MetricProducer.h" #include "frameworks/base/cmds/statsd/src/statsd_config.pb.h" #include "stats_util.h" @@ -36,6 +36,7 @@ struct CountBucket { int64_t mBucketStartNs; int64_t mBucketEndNs; int64_t mCount; + uint64_t mBucketNum; }; class CountMetricProducer : public MetricProducer { @@ -50,6 +51,8 @@ public: void finish() override; + void flushIfNeeded(const uint64_t newEventTime) override; + // TODO: Pass a timestamp as a parameter in onDumpReport. std::unique_ptr> onDumpReport() override; @@ -76,18 +79,15 @@ private: // TODO: Add a lock to mPastBuckets. std::unordered_map> mPastBuckets; - size_t mByteSize; - // The current bucket. std::shared_ptr mCurrentSlicedCounter = std::make_shared(); - vector> mAnomalyTrackers; - - void flushCounterIfNeeded(const uint64_t newEventTime); + static const size_t kBucketSize = sizeof(CountBucket{}); FRIEND_TEST(CountMetricProducerTest, TestNonDimensionalEvents); FRIEND_TEST(CountMetricProducerTest, TestEventsWithNonSlicedCondition); FRIEND_TEST(CountMetricProducerTest, TestEventsWithSlicedCondition); + FRIEND_TEST(CountMetricProducerTest, TestAnomalyDetection); }; } // namespace statsd diff --git a/cmds/statsd/src/metrics/DurationMetricProducer.cpp b/cmds/statsd/src/metrics/DurationMetricProducer.cpp index eba2e06035e56..b0a97b143400d 100644 --- a/cmds/statsd/src/metrics/DurationMetricProducer.cpp +++ b/cmds/statsd/src/metrics/DurationMetricProducer.cpp @@ -110,16 +110,16 @@ void DurationMetricProducer::startNewProtoOutputStream(long long startTime) { } unique_ptr DurationMetricProducer::createDurationTracker( - vector& bucket) { + const HashableDimensionKey& eventKey, vector& bucket) { switch (mMetric.aggregation_type()) { case DurationMetric_AggregationType_SUM: - return make_unique(mWizard, mConditionTrackerIndex, mNested, - mCurrentBucketStartTimeNs, mBucketSizeNs, - bucket); + return make_unique(eventKey, mWizard, mConditionTrackerIndex, + mNested, mCurrentBucketStartTimeNs, + mBucketSizeNs, mAnomalyTrackers, bucket); case DurationMetric_AggregationType_MAX_SPARSE: - return make_unique(mWizard, mConditionTrackerIndex, mNested, - mCurrentBucketStartTimeNs, mBucketSizeNs, - bucket); + return make_unique(eventKey, mWizard, mConditionTrackerIndex, + mNested, mCurrentBucketStartTimeNs, + mBucketSizeNs, mAnomalyTrackers, bucket); } } @@ -131,7 +131,6 @@ void DurationMetricProducer::finish() { void DurationMetricProducer::onSlicedConditionMayChange(const uint64_t eventTime) { VLOG("Metric %s onSlicedConditionMayChange", mMetric.name().c_str()); // Now for each of the on-going event, check if the condition has changed for them. - flushIfNeeded(eventTime); for (auto& pair : mCurrentSlicedDuration) { pair.second->onSlicedConditionMayChange(eventTime); } @@ -142,27 +141,11 @@ void DurationMetricProducer::onConditionChanged(const bool conditionMet, const u mCondition = conditionMet; // TODO: need to populate the condition change time from the event which triggers the condition // change, instead of using current time. - - flushIfNeeded(eventTime); for (auto& pair : mCurrentSlicedDuration) { pair.second->onConditionChanged(conditionMet, eventTime); } } -static void addDurationBucketsToReport(StatsLogReport_DurationMetricDataWrapper& wrapper, - const vector& key, - const vector& buckets) { - DurationMetricData* data = wrapper.add_data(); - for (const auto& kv : key) { - data->add_dimension()->CopyFrom(kv); - } - for (const auto& bucket : buckets) { - data->add_bucket_info()->CopyFrom(bucket); - VLOG("\t bucket [%lld - %lld] duration(ns): %lld", bucket.start_bucket_nanos(), - bucket.end_bucket_nanos(), bucket.duration_nanos()); - } -} - std::unique_ptr> DurationMetricProducer::onDumpReport() { long long endTime = time(nullptr) * NS_PER_SEC; @@ -231,7 +214,6 @@ void DurationMetricProducer::flushIfNeeded(uint64_t eventTime) { if (mCurrentBucketStartTimeNs + mBucketSizeNs > eventTime) { return; } - VLOG("flushing..........."); for (auto it = mCurrentSlicedDuration.begin(); it != mCurrentSlicedDuration.end();) { if (it->second->flushIfNeeded(eventTime)) { @@ -244,6 +226,7 @@ void DurationMetricProducer::flushIfNeeded(uint64_t eventTime) { int numBucketsForward = (eventTime - mCurrentBucketStartTimeNs) / mBucketSizeNs; mCurrentBucketStartTimeNs += numBucketsForward * mBucketSizeNs; + mCurrentBucketNum += numBucketsForward; } void DurationMetricProducer::onMatchedLogEventInternal( @@ -262,7 +245,7 @@ void DurationMetricProducer::onMatchedLogEventInternal( HashableDimensionKey atomKey = getHashableKey(getDimensionKey(event, mInternalDimension)); if (mCurrentSlicedDuration.find(eventKey) == mCurrentSlicedDuration.end()) { - mCurrentSlicedDuration[eventKey] = createDurationTracker(mPastBuckets[eventKey]); + mCurrentSlicedDuration[eventKey] = createDurationTracker(eventKey, mPastBuckets[eventKey]); } auto it = mCurrentSlicedDuration.find(eventKey); diff --git a/cmds/statsd/src/metrics/DurationMetricProducer.h b/cmds/statsd/src/metrics/DurationMetricProducer.h index bb5d4d94dea8c..4c8dbcb83a6bc 100644 --- a/cmds/statsd/src/metrics/DurationMetricProducer.h +++ b/cmds/statsd/src/metrics/DurationMetricProducer.h @@ -14,8 +14,8 @@ * limitations under the License. */ -#ifndef DURATION_METRIC_PRODUCER_H -#define DURATION_METRIC_PRODUCER_H +#pragma once + #include @@ -48,6 +48,7 @@ public: void onConditionChanged(const bool conditionMet, const uint64_t eventTime) override; void finish() override; + void flushIfNeeded(const uint64_t newEventTime) override; // TODO: Pass a timestamp as a parameter in onDumpReport. std::unique_ptr> onDumpReport() override; @@ -95,11 +96,8 @@ private: std::unordered_map> mCurrentSlicedDuration; - void flushDurationIfNeeded(const uint64_t newEventTime); - - std::unique_ptr createDurationTracker(std::vector& bucket); - - void flushIfNeeded(uint64_t timestamp); + std::unique_ptr createDurationTracker(const HashableDimensionKey& eventKey, + std::vector& bucket); static const size_t kBucketSize = sizeof(DurationBucket{}); }; @@ -108,4 +106,3 @@ private: } // namespace os } // namespace android -#endif // DURATION_METRIC_PRODUCER_H diff --git a/cmds/statsd/src/metrics/EventMetricProducer.h b/cmds/statsd/src/metrics/EventMetricProducer.h index 0dccdf4938695..0c453cdac99be 100644 --- a/cmds/statsd/src/metrics/EventMetricProducer.h +++ b/cmds/statsd/src/metrics/EventMetricProducer.h @@ -47,6 +47,8 @@ public: void onConditionChanged(const bool conditionMet, const uint64_t eventTime) override; void finish() override; + void flushIfNeeded(const uint64_t newEventTime) override { + } // TODO: Pass a timestamp as a parameter in onDumpReport. std::unique_ptr> onDumpReport() override; diff --git a/cmds/statsd/src/metrics/GaugeMetricProducer.cpp b/cmds/statsd/src/metrics/GaugeMetricProducer.cpp index ed18f897943ee..42ac1a2bbd7b6 100644 --- a/cmds/statsd/src/metrics/GaugeMetricProducer.cpp +++ b/cmds/statsd/src/metrics/GaugeMetricProducer.cpp @@ -63,8 +63,9 @@ const int FIELD_ID_END_BUCKET_NANOS = 2; const int FIELD_ID_GAUGE = 3; GaugeMetricProducer::GaugeMetricProducer(const GaugeMetric& metric, const int conditionIndex, - const sp& wizard, const int pullTagId) - : MetricProducer((time(nullptr) * NS_PER_SEC), conditionIndex, wizard), + const sp& wizard, const int pullTagId, + const int64_t startTimeNs) + : MetricProducer(startTimeNs, conditionIndex, wizard), mMetric(metric), mPullTagId(pullTagId) { if (metric.has_bucket() && metric.bucket().has_bucket_size_millis()) { @@ -114,7 +115,7 @@ std::unique_ptr> GaugeMetricProducer::onDumpReport() { // Dump current bucket if it's stale. // If current bucket is still on-going, don't force dump current bucket. // In finish(), We can force dump current bucket. - flushGaugeIfNeededLocked(time(nullptr) * NS_PER_SEC); + flushIfNeeded(time(nullptr) * NS_PER_SEC); for (const auto& pair : mPastBuckets) { const HashableDimensionKey& hashableKey = pair.first; @@ -168,7 +169,6 @@ std::unique_ptr> GaugeMetricProducer::onDumpReport() { startNewProtoOutputStream(time(nullptr) * NS_PER_SEC); mPastBuckets.clear(); - mByteSize = 0; return buffer; @@ -178,15 +178,18 @@ std::unique_ptr> GaugeMetricProducer::onDumpReport() { void GaugeMetricProducer::onConditionChanged(const bool conditionMet, const uint64_t eventTime) { AutoMutex _l(mLock); VLOG("Metric %s onConditionChanged", mMetric.name().c_str()); + flushIfNeeded(eventTime); mCondition = conditionMet; - // Push mode. Nothing to do. + // Push mode. No need to proactively pull the gauge data. if (mPullTagId == -1) { return; } - // If (1) the condition is not met or (2) we already pulled the gauge metric in the current - // bucket, do not pull gauge again. - if (!mCondition || mCurrentSlicedBucket.size() > 0) { + if (!mCondition) { + return; + } + // Already have gauge metric for the current bucket, do not do it again. + if (mCurrentSlicedBucket->size() > 0) { return; } vector> allData; @@ -197,16 +200,16 @@ void GaugeMetricProducer::onConditionChanged(const bool conditionMet, const uint for (const auto& data : allData) { onMatchedLogEvent(0, *data, false /*scheduledPull*/); } - flushGaugeIfNeededLocked(eventTime); + flushIfNeeded(eventTime); } void GaugeMetricProducer::onSlicedConditionMayChange(const uint64_t eventTime) { VLOG("Metric %s onSlicedConditionMayChange", mMetric.name().c_str()); } -long GaugeMetricProducer::getGauge(const LogEvent& event) { +int64_t GaugeMetricProducer::getGauge(const LogEvent& event) { status_t err = NO_ERROR; - long val = event.GetLong(mMetric.gauge_field(), &err); + int64_t val = event.GetLong(mMetric.gauge_field(), &err); if (err == NO_ERROR) { return val; } else { @@ -217,14 +220,9 @@ long GaugeMetricProducer::getGauge(const LogEvent& event) { void GaugeMetricProducer::onDataPulled(const std::vector>& allData) { AutoMutex mutex(mLock); - if (allData.size() == 0) { - return; - } for (const auto& data : allData) { onMatchedLogEvent(0, *data, true /*scheduledPull*/); } - uint64_t eventTime = allData.at(0)->GetTimestampNs(); - flushGaugeIfNeededLocked(eventTime); } void GaugeMetricProducer::onMatchedLogEventInternal( @@ -241,15 +239,21 @@ void GaugeMetricProducer::onMatchedLogEventInternal( return; } - // For gauge metric, we just simply use the latest guage in the given bucket. - const long gauge = getGauge(event); - if (gauge < 0) { - VLOG("Invalid gauge at event Time: %lld", (long long)eventTimeNs); + // When the event happens in a new bucket, flush the old buckets. + if (eventTimeNs >= mCurrentBucketStartTimeNs + mBucketSizeNs) { + flushIfNeeded(eventTimeNs); + } + + // For gauge metric, we just simply use the first guage in the given bucket. + if (!mCurrentSlicedBucket->empty()) { return; } - mCurrentSlicedBucket[eventKey] = gauge; - if (mPullTagId < 0) { - flushGaugeIfNeededLocked(eventTimeNs); + const long gauge = getGauge(event); + if (gauge >= 0) { + (*mCurrentSlicedBucket)[eventKey] = gauge; + } + for (auto& tracker : mAnomalyTrackers) { + tracker->detectAndDeclareAnomaly(eventTimeNs, mCurrentBucketNum, eventKey, gauge); } } @@ -258,39 +262,45 @@ void GaugeMetricProducer::onMatchedLogEventInternal( // bucket. // if data is pushed, onMatchedLogEvent will only be called through onConditionChanged() inside // the GaugeMetricProducer while holding the lock. -void GaugeMetricProducer::flushGaugeIfNeededLocked(const uint64_t eventTimeNs) { - if (mCurrentBucketStartTimeNs + mBucketSizeNs > eventTimeNs) { - VLOG("event time is %lld, less than next bucket start time %lld", (long long)eventTimeNs, - (long long)(mCurrentBucketStartTimeNs + mBucketSizeNs)); +void GaugeMetricProducer::flushIfNeeded(const uint64_t eventTimeNs) { + if (eventTimeNs < mCurrentBucketStartTimeNs + mBucketSizeNs) { return; } - // Adjusts the bucket start time - int64_t numBucketsForward = (eventTimeNs - mCurrentBucketStartTimeNs) / mBucketSizeNs; - GaugeBucket info; info.mBucketStartNs = mCurrentBucketStartTimeNs; info.mBucketEndNs = mCurrentBucketStartTimeNs + mBucketSizeNs; + info.mBucketNum = mCurrentBucketNum; - for (const auto& slice : mCurrentSlicedBucket) { + for (const auto& slice : *mCurrentSlicedBucket) { info.mGauge = slice.second; auto& bucketList = mPastBuckets[slice.first]; bucketList.push_back(info); - mByteSize += sizeof(info); - - VLOG("gauge metric %s, dump key value: %s -> %ld", mMetric.name().c_str(), - slice.first.c_str(), slice.second); + VLOG("gauge metric %s, dump key value: %s -> %lld", mMetric.name().c_str(), + slice.first.c_str(), (long long)slice.second); } - // Reset counters - mCurrentSlicedBucket.clear(); + // Reset counters + for (auto& tracker : mAnomalyTrackers) { + tracker->addPastBucket(mCurrentSlicedBucket, mCurrentBucketNum); + } + + mCurrentSlicedBucket = std::make_shared(); + + // Adjusts the bucket start time + int64_t numBucketsForward = (eventTimeNs - mCurrentBucketStartTimeNs) / mBucketSizeNs; mCurrentBucketStartTimeNs = mCurrentBucketStartTimeNs + numBucketsForward * mBucketSizeNs; + mCurrentBucketNum += numBucketsForward; VLOG("metric %s: new bucket start time: %lld", mMetric.name().c_str(), (long long)mCurrentBucketStartTimeNs); } size_t GaugeMetricProducer::byteSize() { - return mByteSize; + size_t totalSize = 0; + for (const auto& pair : mPastBuckets) { + totalSize += pair.second.size() * kBucketSize; + } + return totalSize; } } // namespace statsd diff --git a/cmds/statsd/src/metrics/GaugeMetricProducer.h b/cmds/statsd/src/metrics/GaugeMetricProducer.h index f9e4deb9f04e9..930afb21543f1 100644 --- a/cmds/statsd/src/metrics/GaugeMetricProducer.h +++ b/cmds/statsd/src/metrics/GaugeMetricProducer.h @@ -19,6 +19,7 @@ #include #include +#include #include "../condition/ConditionTracker.h" #include "../external/PullDataReceiver.h" #include "../external/StatsPullerManager.h" @@ -35,6 +36,7 @@ struct GaugeBucket { int64_t mBucketStartNs; int64_t mBucketEndNs; int64_t mGauge; + uint64_t mBucketNum; }; // This gauge metric producer first register the puller to automatically pull the gauge at the @@ -46,7 +48,8 @@ public: // TODO: Pass in the start time from MetricsManager, it should be consistent // for all metrics. GaugeMetricProducer(const GaugeMetric& countMetric, const int conditionIndex, - const sp& wizard, const int pullTagId); + const sp& wizard, const int pullTagId, + const int64_t startTimeNs); virtual ~GaugeMetricProducer(); @@ -57,6 +60,7 @@ public: void onSlicedConditionMayChange(const uint64_t eventTime) override; void finish() override; + void flushIfNeeded(const uint64_t newEventTime) override; // TODO: Pass a timestamp as a parameter in onDumpReport. std::unique_ptr> onDumpReport() override; @@ -92,13 +96,15 @@ private: std::unordered_map> mPastBuckets; // The current bucket. - std::unordered_map mCurrentSlicedBucket; + std::shared_ptr mCurrentSlicedBucket = std::make_shared(); - void flushGaugeIfNeededLocked(const uint64_t newEventTime); + int64_t getGauge(const LogEvent& event); - long getGauge(const LogEvent& event); + static const size_t kBucketSize = sizeof(GaugeBucket{}); - size_t mByteSize; + FRIEND_TEST(GaugeMetricProducerTest, TestWithCondition); + FRIEND_TEST(GaugeMetricProducerTest, TestNoCondition); + FRIEND_TEST(GaugeMetricProducerTest, TestAnomalyDetection); }; } // namespace statsd diff --git a/cmds/statsd/src/metrics/MetricProducer.h b/cmds/statsd/src/metrics/MetricProducer.h index c7982a837c140..0f93744299306 100644 --- a/cmds/statsd/src/metrics/MetricProducer.h +++ b/cmds/statsd/src/metrics/MetricProducer.h @@ -17,6 +17,7 @@ #ifndef METRIC_PRODUCER_H #define METRIC_PRODUCER_H +#include "anomaly/AnomalyTracker.h" #include "condition/ConditionWizard.h" #include "matchers/matcher_util.h" #include "packages/PackageInfoListener.h" @@ -58,6 +59,7 @@ public: // This is called when the metric collecting is done, e.g., when there is a new configuration // coming. MetricProducer should do the clean up, and dump existing data to dropbox. virtual void finish() = 0; + virtual void flushIfNeeded(const uint64_t newEventTime) = 0; // TODO: Pass a timestamp as a parameter in onDumpReport and update all its // implementations. @@ -72,6 +74,14 @@ public: // state. virtual size_t byteSize() = 0; + void addAnomalyTracker(sp tracker) { + mAnomalyTrackers.push_back(tracker); + } + + int64_t getBuckeSizeInNs() const { + return mBucketSizeNs; + } + protected: const uint64_t mStartTimeNs; @@ -97,6 +107,8 @@ protected: std::vector mConditionLinks; + std::vector> mAnomalyTrackers; + /* * Individual metrics can implement their own business logic here. All pre-processing is done. * diff --git a/cmds/statsd/src/metrics/MetricsManager.cpp b/cmds/statsd/src/metrics/MetricsManager.cpp index e8a862fef79b6..5916b040889cc 100644 --- a/cmds/statsd/src/metrics/MetricsManager.cpp +++ b/cmds/statsd/src/metrics/MetricsManager.cpp @@ -38,8 +38,8 @@ namespace statsd { MetricsManager::MetricsManager(const StatsdConfig& config) { mConfigValid = initStatsdConfig(config, mTagIds, mAllLogEntryMatchers, mAllConditionTrackers, - mAllMetricProducers, mConditionToMetricMap, mTrackerToMetricMap, - mTrackerToConditionMap); + mAllMetricProducers, mAllAnomalyTrackers, mConditionToMetricMap, + mTrackerToMetricMap, mTrackerToConditionMap); } MetricsManager::~MetricsManager() { @@ -150,6 +150,19 @@ void MetricsManager::onLogEvent(const LogEvent& event) { } } +void MetricsManager::onAnomalyAlarmFired(const uint64_t timestampNs, + sp anomaly) { + for (const auto& itr : mAllAnomalyTrackers) { + itr->declareAnomaly(timestampNs); + } +} + +void MetricsManager::setAnomalyMonitor(const sp& anomalyMonitor) { + for (auto& itr : mAllAnomalyTrackers) { + itr->setAnomalyMonitor(anomalyMonitor); + } +} + // Returns the total byte size of all metrics managed by a single config source. size_t MetricsManager::byteSize() { size_t totalSize = 0; diff --git a/cmds/statsd/src/metrics/MetricsManager.h b/cmds/statsd/src/metrics/MetricsManager.h index fb167798a44fb..59ade7cdf48b2 100644 --- a/cmds/statsd/src/metrics/MetricsManager.h +++ b/cmds/statsd/src/metrics/MetricsManager.h @@ -16,6 +16,8 @@ #pragma once +#include "anomaly/AnomalyMonitor.h" +#include "anomaly/AnomalyTracker.h" #include "condition/ConditionTracker.h" #include "frameworks/base/cmds/statsd/src/statsd_config.pb.h" #include "logd/LogEvent.h" @@ -43,6 +45,10 @@ public: // Called when everything should wrap up. We are about to finish (e.g., new config comes). void finish(); + void onAnomalyAlarmFired(const uint64_t timestampNs, sp anomaly); + + void setAnomalyMonitor(const sp& anomalyMonitor); + // Config source owner can call onDumpReport() to get all the metrics collected. std::vector>> onDumpReport(); @@ -70,6 +76,9 @@ private: // Hold all metrics from the config. std::vector> mAllMetricProducers; + // Hold all alert trackers. + std::vector> mAllAnomalyTrackers; + // To make the log processing more efficient, we want to do as much filtering as possible // before we go into individual trackers and conditions to match. diff --git a/cmds/statsd/src/metrics/ValueMetricProducer.cpp b/cmds/statsd/src/metrics/ValueMetricProducer.cpp index a35070bab54ba..9cbe6f6eac153 100644 --- a/cmds/statsd/src/metrics/ValueMetricProducer.cpp +++ b/cmds/statsd/src/metrics/ValueMetricProducer.cpp @@ -188,7 +188,6 @@ std::unique_ptr> ValueMetricProducer::onDumpReport() { startNewProtoOutputStream(time(nullptr) * NS_PER_SEC); mPastBuckets.clear(); - mByteSize = 0; return buffer; @@ -215,7 +214,7 @@ void ValueMetricProducer::onConditionChanged(const bool condition, const uint64_ for (const auto& data : allData) { onMatchedLogEvent(0, *data, false); } - flush_if_needed(eventTime); + flushIfNeeded(eventTime); } return; } @@ -230,12 +229,12 @@ void ValueMetricProducer::onDataPulled(const std::vectorGetTimestampNs(); // alarm is not accurate and might drift. if (eventTime > mCurrentBucketStartTimeNs + mBucketSizeNs * 3 / 2) { - flush_if_needed(eventTime); + flushIfNeeded(eventTime); } for (const auto& data : allData) { onMatchedLogEvent(0, *data, true); } - flush_if_needed(eventTime); + flushIfNeeded(eventTime); } } @@ -282,7 +281,7 @@ void ValueMetricProducer::onMatchedLogEventInternal( } } } else { - flush_if_needed(eventTimeNs); + flushIfNeeded(eventTimeNs); interval.raw.push_back(make_pair(value, 0)); } } @@ -298,18 +297,18 @@ long ValueMetricProducer::get_value(const LogEvent& event) { } } -void ValueMetricProducer::flush_if_needed(const uint64_t eventTimeNs) { +void ValueMetricProducer::flushIfNeeded(const uint64_t eventTimeNs) { if (mCurrentBucketStartTimeNs + mBucketSizeNs > eventTimeNs) { VLOG("eventTime is %lld, less than next bucket start time %lld", (long long)eventTimeNs, (long long)(mCurrentBucketStartTimeNs + mBucketSizeNs)); return; } - VLOG("finalizing bucket for %ld, dumping %d slices", (long)mCurrentBucketStartTimeNs, (int)mCurrentSlicedBucket.size()); ValueBucket info; info.mBucketStartNs = mCurrentBucketStartTimeNs; info.mBucketEndNs = mCurrentBucketStartTimeNs + mBucketSizeNs; + info.mBucketNum = mCurrentBucketNum; int tainted = 0; for (const auto& slice : mCurrentSlicedBucket) { @@ -329,23 +328,29 @@ void ValueMetricProducer::flush_if_needed(const uint64_t eventTimeNs) { // it will auto create new vector of ValuebucketInfo if the key is not found. auto& bucketList = mPastBuckets[slice.first]; bucketList.push_back(info); - mByteSize += sizeof(info); } // Reset counters mCurrentSlicedBucket.swap(mNextSlicedBucket); mNextSlicedBucket.clear(); + int64_t numBucketsForward = (eventTimeNs - mCurrentBucketStartTimeNs) / mBucketSizeNs; + mCurrentBucketStartTimeNs = mCurrentBucketStartTimeNs + numBucketsForward * mBucketSizeNs; + mCurrentBucketNum += numBucketsForward; + if (numBucketsForward > 1) { VLOG("Skipping forward %lld buckets", (long long)numBucketsForward); } - mCurrentBucketStartTimeNs = mCurrentBucketStartTimeNs + numBucketsForward * mBucketSizeNs; VLOG("metric %s: new bucket start time: %lld", mMetric.name().c_str(), (long long)mCurrentBucketStartTimeNs); } size_t ValueMetricProducer::byteSize() { - return mByteSize; + size_t totalSize = 0; + for (const auto& pair : mPastBuckets) { + totalSize += pair.second.size() * kBucketSize; + } + return totalSize; } } // namespace statsd diff --git a/cmds/statsd/src/metrics/ValueMetricProducer.h b/cmds/statsd/src/metrics/ValueMetricProducer.h index c6c87f5b15f67..2b0b0ada2a8f9 100644 --- a/cmds/statsd/src/metrics/ValueMetricProducer.h +++ b/cmds/statsd/src/metrics/ValueMetricProducer.h @@ -33,6 +33,7 @@ struct ValueBucket { int64_t mBucketStartNs; int64_t mBucketEndNs; int64_t mValue; + uint64_t mBucketNum; }; class ValueMetricProducer : public virtual MetricProducer, public virtual PullDataReceiver { @@ -46,6 +47,7 @@ public: void onConditionChanged(const bool condition, const uint64_t eventTime) override; void finish() override; + void flushIfNeeded(const uint64_t eventTimeNs) override; // TODO: Pass a timestamp as a parameter in onDumpReport. std::unique_ptr> onDumpReport() override; @@ -102,9 +104,7 @@ private: long get_value(const LogEvent& event); - void flush_if_needed(const uint64_t eventTimeNs); - - size_t mByteSize; + static const size_t kBucketSize = sizeof(ValueBucket{}); FRIEND_TEST(ValueMetricProducerTest, TestNonDimensionalEvents); FRIEND_TEST(ValueMetricProducerTest, TestEventsWithNonSlicedCondition); diff --git a/cmds/statsd/src/metrics/duration_helper/DurationTracker.h b/cmds/statsd/src/metrics/duration_helper/DurationTracker.h index 18b334988d0db..c91ea0f845bde 100644 --- a/cmds/statsd/src/metrics/duration_helper/DurationTracker.h +++ b/cmds/statsd/src/metrics/duration_helper/DurationTracker.h @@ -17,6 +17,7 @@ #ifndef DURATION_TRACKER_H #define DURATION_TRACKER_H +#include "anomaly/AnomalyTracker.h" #include "condition/ConditionWizard.h" #include "stats_util.h" @@ -50,23 +51,28 @@ struct DurationInfo { }; struct DurationBucket { - int64_t mBucketStartNs; - int64_t mBucketEndNs; - int64_t mDuration; + uint64_t mBucketStartNs; + uint64_t mBucketEndNs; + uint64_t mDuration; + uint64_t mBucketNum; }; class DurationTracker { public: - DurationTracker(sp wizard, int conditionIndex, bool nesting, - uint64_t currentBucketStartNs, uint64_t bucketSizeNs, + DurationTracker(const HashableDimensionKey& eventKey, sp wizard, + int conditionIndex, bool nesting, uint64_t currentBucketStartNs, + uint64_t bucketSizeNs, const std::vector>& anomalyTrackers, std::vector& bucket) - : mWizard(wizard), + : mEventKey(eventKey), + mWizard(wizard), mConditionTrackerIndex(conditionIndex), mBucketSizeNs(bucketSizeNs), mNested(nesting), mCurrentBucketStartTimeNs(currentBucketStartNs), mBucket(bucket), - mDuration(0){}; + mDuration(0), + mCurrentBucketNum(0), + mAnomalyTrackers(anomalyTrackers){}; virtual ~DurationTracker(){}; virtual void noteStart(const HashableDimensionKey& key, bool condition, const uint64_t eventTime, const ConditionKey& conditionKey) = 0; @@ -79,7 +85,58 @@ public: // events, so that the owner can safely remove the tracker. virtual bool flushIfNeeded(uint64_t timestampNs) = 0; + // Predict the anomaly timestamp given the current status. + virtual int64_t predictAnomalyTimestampNs(const AnomalyTracker& anomalyTracker, + const uint64_t currentTimestamp) const = 0; + protected: + // Starts the anomaly alarm. + void startAnomalyAlarm(const uint64_t eventTime) { + for (auto& anomalyTracker : mAnomalyTrackers) { + if (anomalyTracker != nullptr) { + anomalyTracker->startAlarm(mEventKey, + predictAnomalyTimestampNs(*anomalyTracker, eventTime)); + } + } + } + + // Stops the anomaly alarm. + void stopAnomalyAlarm() { + for (auto& anomalyTracker : mAnomalyTrackers) { + if (anomalyTracker != nullptr) { + anomalyTracker->stopAlarm(mEventKey); + } + } + } + + void addPastBucketToAnomalyTrackers(const int64_t& bucketValue, const int64_t& bucketNum) { + for (auto& anomalyTracker : mAnomalyTrackers) { + if (anomalyTracker != nullptr) { + anomalyTracker->addPastBucket(mEventKey, bucketValue, bucketNum); + } + } + } + + void detectAndDeclareAnomaly(const uint64_t& timestamp, const int64_t& currBucketNum, + const int64_t& currentBucketValue) { + for (auto& anomalyTracker : mAnomalyTrackers) { + if (anomalyTracker != nullptr) { + anomalyTracker->detectAndDeclareAnomaly(timestamp, currBucketNum, mEventKey, + currentBucketValue); + } + } + } + + void declareAnomalyIfAlarmExpired(const uint64_t& timestamp) { + for (auto& anomalyTracker : mAnomalyTrackers) { + if (anomalyTracker != nullptr) { + anomalyTracker->declareAnomalyIfAlarmExpired(mEventKey, timestamp); + } + } + } + + HashableDimensionKey mEventKey; + sp mWizard; const int mConditionTrackerIndex; @@ -93,6 +150,13 @@ protected: std::vector& mBucket; // where to write output int64_t mDuration; // current recorded duration result + + uint64_t mCurrentBucketNum; + + std::vector> mAnomalyTrackers; + + FRIEND_TEST(OringDurationTrackerTest, TestPredictAnomalyTimestamp); + FRIEND_TEST(OringDurationTrackerTest, TestAnomalyDetection); }; } // namespace statsd diff --git a/cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.cpp b/cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.cpp index 8c7bfb6e4462d..06e743daa0909 100644 --- a/cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.cpp +++ b/cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.cpp @@ -23,14 +23,18 @@ namespace android { namespace os { namespace statsd { -MaxDurationTracker::MaxDurationTracker(sp wizard, int conditionIndex, bool nesting, +MaxDurationTracker::MaxDurationTracker(const HashableDimensionKey& eventKey, + sp wizard, int conditionIndex, bool nesting, uint64_t currentBucketStartNs, uint64_t bucketSizeNs, + const std::vector>& anomalyTrackers, std::vector& bucket) - : DurationTracker(wizard, conditionIndex, nesting, currentBucketStartNs, bucketSizeNs, bucket) { + : DurationTracker(eventKey, wizard, conditionIndex, nesting, currentBucketStartNs, bucketSizeNs, + anomalyTrackers, bucket) { } void MaxDurationTracker::noteStart(const HashableDimensionKey& key, bool condition, const uint64_t eventTime, const ConditionKey& conditionKey) { + flushIfNeeded(eventTime); // this will construct a new DurationInfo if this key didn't exist. DurationInfo& duration = mInfos[key]; duration.conditionKeys = conditionKey; @@ -56,8 +60,11 @@ void MaxDurationTracker::noteStart(const HashableDimensionKey& key, bool conditi } } + void MaxDurationTracker::noteStop(const HashableDimensionKey& key, const uint64_t eventTime, bool forceStop) { + flushIfNeeded(eventTime); + declareAnomalyIfAlarmExpired(eventTime); VLOG("MaxDuration: key %s stop", key.c_str()); if (mInfos.find(key) == mInfos.end()) { // we didn't see a start event before. do nothing. @@ -78,6 +85,7 @@ void MaxDurationTracker::noteStop(const HashableDimensionKey& key, const uint64_ (long long)duration.lastStartTime, (long long)eventTime, (long long)durationTime); duration.lastDuration = duration.lastDuration + durationTime; + duration.lastStartTime = -1; VLOG(" record duration: %lld ", (long long)duration.lastDuration); } break; @@ -93,6 +101,7 @@ void MaxDurationTracker::noteStop(const HashableDimensionKey& key, const uint64_ if (duration.lastDuration > mDuration) { mDuration = duration.lastDuration; + detectAndDeclareAnomaly(eventTime, mCurrentBucketNum, mDuration); VLOG("Max: new max duration: %lld", (long long)mDuration); } // Once an atom duration ends, we erase it. Next time, if we see another atom event with the @@ -101,9 +110,14 @@ void MaxDurationTracker::noteStop(const HashableDimensionKey& key, const uint64_ mInfos.erase(key); } } + void MaxDurationTracker::noteStopAll(const uint64_t eventTime) { - for (auto& pair : mInfos) { - noteStop(pair.first, eventTime, true); + std::set keys; + for (const auto& pair : mInfos) { + keys.insert(pair.first); + } + for (auto& key : keys) { + noteStop(key, eventTime, true); } } @@ -122,7 +136,7 @@ bool MaxDurationTracker::flushIfNeeded(uint64_t eventTime) { DurationBucket info; info.mBucketStartNs = mCurrentBucketStartTimeNs; info.mBucketEndNs = endTime; - + info.mBucketNum = mCurrentBucketNum; uint64_t oldBucketStartTimeNs = mCurrentBucketStartTimeNs; mCurrentBucketStartTimeNs += (numBucketsForward)*mBucketSizeNs; @@ -165,6 +179,7 @@ bool MaxDurationTracker::flushIfNeeded(uint64_t eventTime) { if (mDuration != 0) { info.mDuration = mDuration; mBucket.push_back(info); + addPastBucketToAnomalyTrackers(info.mDuration, info.mBucketNum); VLOG(" final duration for last bucket: %lld", (long long)mDuration); } @@ -174,11 +189,15 @@ bool MaxDurationTracker::flushIfNeeded(uint64_t eventTime) { DurationBucket info; info.mBucketStartNs = oldBucketStartTimeNs + mBucketSizeNs * i; info.mBucketEndNs = endTime + mBucketSizeNs * i; + info.mBucketNum = mCurrentBucketNum + i; info.mDuration = mBucketSizeNs; mBucket.push_back(info); + addPastBucketToAnomalyTrackers(info.mDuration, info.mBucketNum); VLOG(" filling gap bucket with duration %lld", (long long)mBucketSizeNs); } } + + mCurrentBucketNum += numBucketsForward; // If this tracker has no pending events, tell owner to remove. return !hasPendingEvent; } @@ -204,6 +223,8 @@ void MaxDurationTracker::onConditionChanged(bool condition, const uint64_t times void MaxDurationTracker::noteConditionChanged(const HashableDimensionKey& key, bool conditionMet, const uint64_t timestamp) { + flushIfNeeded(timestamp); + declareAnomalyIfAlarmExpired(timestamp); auto it = mInfos.find(key); if (it == mInfos.end()) { return; @@ -215,7 +236,6 @@ void MaxDurationTracker::noteConditionChanged(const HashableDimensionKey& key, b if (!conditionMet) { it->second.state = DurationState::kPaused; it->second.lastDuration += (timestamp - it->second.lastStartTime); - VLOG("MaxDurationTracker Key: %s Started->Paused ", key.c_str()); } break; @@ -232,6 +252,16 @@ void MaxDurationTracker::noteConditionChanged(const HashableDimensionKey& key, b } break; } + if (it->second.lastDuration > mDuration) { + mDuration = it->second.lastDuration; + detectAndDeclareAnomaly(timestamp, mCurrentBucketNum, mDuration); + } +} + +int64_t MaxDurationTracker::predictAnomalyTimestampNs(const AnomalyTracker& anomalyTracker, + const uint64_t currentTimestamp) const { + ALOGE("Max duration producer does not support anomaly timestamp prediction!!!"); + return currentTimestamp; } } // namespace statsd diff --git a/cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.h b/cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.h index 167f81e414e12..ca1021041e34c 100644 --- a/cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.h +++ b/cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.h @@ -28,8 +28,10 @@ namespace statsd { // they stop or bucket expires. class MaxDurationTracker : public DurationTracker { public: - MaxDurationTracker(sp wizard, int conditionIndex, bool nesting, - uint64_t currentBucketStartNs, uint64_t bucketSizeNs, + MaxDurationTracker(const HashableDimensionKey& eventKey, sp wizard, + int conditionIndex, bool nesting, uint64_t currentBucketStartNs, + uint64_t bucketSizeNs, + const std::vector>& anomalyTrackers, std::vector& bucket); void noteStart(const HashableDimensionKey& key, bool condition, const uint64_t eventTime, const ConditionKey& conditionKey) override; @@ -40,11 +42,20 @@ public: void onSlicedConditionMayChange(const uint64_t timestamp) override; void onConditionChanged(bool condition, const uint64_t timestamp) override; + int64_t predictAnomalyTimestampNs(const AnomalyTracker& anomalyTracker, + const uint64_t currentTimestamp) const override; + private: std::map mInfos; void noteConditionChanged(const HashableDimensionKey& key, bool conditionMet, const uint64_t timestamp); + + FRIEND_TEST(MaxDurationTrackerTest, TestSimpleMaxDuration); + FRIEND_TEST(MaxDurationTrackerTest, TestCrossBucketBoundary); + FRIEND_TEST(MaxDurationTrackerTest, TestMaxDurationWithCondition); + FRIEND_TEST(MaxDurationTrackerTest, TestStopAll); + FRIEND_TEST(MaxDurationTrackerTest, TestAnomalyDetection); }; } // namespace statsd diff --git a/cmds/statsd/src/metrics/duration_helper/OringDurationTracker.cpp b/cmds/statsd/src/metrics/duration_helper/OringDurationTracker.cpp index faf5ce50b45f5..29b6c8965ee60 100644 --- a/cmds/statsd/src/metrics/duration_helper/OringDurationTracker.cpp +++ b/cmds/statsd/src/metrics/duration_helper/OringDurationTracker.cpp @@ -23,11 +23,14 @@ namespace statsd { using std::pair; -OringDurationTracker::OringDurationTracker(sp wizard, int conditionIndex, +OringDurationTracker::OringDurationTracker(const HashableDimensionKey& eventKey, + sp wizard, int conditionIndex, bool nesting, uint64_t currentBucketStartNs, uint64_t bucketSizeNs, + const std::vector>& anomalyTrackers, std::vector& bucket) - : DurationTracker(wizard, conditionIndex, nesting, currentBucketStartNs, bucketSizeNs, bucket), + : DurationTracker(eventKey, wizard, conditionIndex, nesting, currentBucketStartNs, bucketSizeNs, + anomalyTrackers, bucket), mStarted(), mPaused() { mLastStartTime = 0; @@ -35,10 +38,12 @@ OringDurationTracker::OringDurationTracker(sp wizard, int condi void OringDurationTracker::noteStart(const HashableDimensionKey& key, bool condition, const uint64_t eventTime, const ConditionKey& conditionKey) { + flushIfNeeded(eventTime); if (condition) { if (mStarted.size() == 0) { mLastStartTime = eventTime; VLOG("record first start...."); + startAnomalyAlarm(eventTime); } mStarted[key]++; } else { @@ -54,6 +59,8 @@ void OringDurationTracker::noteStart(const HashableDimensionKey& key, bool condi void OringDurationTracker::noteStop(const HashableDimensionKey& key, const uint64_t timestamp, const bool stopAll) { + flushIfNeeded(timestamp); + declareAnomalyIfAlarmExpired(timestamp); VLOG("Oring: %s stop", key.c_str()); auto it = mStarted.find(key); if (it != mStarted.end()) { @@ -64,6 +71,8 @@ void OringDurationTracker::noteStop(const HashableDimensionKey& key, const uint6 } if (mStarted.empty()) { mDuration += (timestamp - mLastStartTime); + detectAndDeclareAnomaly(timestamp, mCurrentBucketNum, mDuration); + mLastStartTime = -1; VLOG("record duration %lld, total %lld ", (long long)timestamp - mLastStartTime, (long long)mDuration); } @@ -76,56 +85,70 @@ void OringDurationTracker::noteStop(const HashableDimensionKey& key, const uint6 mPaused.erase(pausedIt); mConditionKeyMap.erase(key); } - } + } + if (mStarted.empty()) { + stopAnomalyAlarm(); + } } + void OringDurationTracker::noteStopAll(const uint64_t timestamp) { + flushIfNeeded(timestamp); + declareAnomalyIfAlarmExpired(timestamp); if (!mStarted.empty()) { mDuration += (timestamp - mLastStartTime); VLOG("Oring Stop all: record duration %lld %lld ", (long long)timestamp - mLastStartTime, (long long)mDuration); + detectAndDeclareAnomaly(timestamp, mCurrentBucketNum, mDuration); } + stopAnomalyAlarm(); mStarted.clear(); mPaused.clear(); mConditionKeyMap.clear(); + mLastStartTime = -1; } bool OringDurationTracker::flushIfNeeded(uint64_t eventTime) { - if (mCurrentBucketStartTimeNs + mBucketSizeNs > eventTime) { + if (eventTime < mCurrentBucketStartTimeNs + mBucketSizeNs) { return false; } VLOG("OringDurationTracker Flushing............."); // adjust the bucket start time int numBucketsForward = (eventTime - mCurrentBucketStartTimeNs) / mBucketSizeNs; - DurationBucket info; - uint64_t endTime = mCurrentBucketStartTimeNs + mBucketSizeNs; - info.mBucketStartNs = mCurrentBucketStartTimeNs; - info.mBucketEndNs = endTime; - - uint64_t oldBucketStartTimeNs = mCurrentBucketStartTimeNs; - mCurrentBucketStartTimeNs += (numBucketsForward)*mBucketSizeNs; - + DurationBucket current_info; + current_info.mBucketStartNs = mCurrentBucketStartTimeNs; + current_info.mBucketEndNs = current_info.mBucketStartNs + mBucketSizeNs; + current_info.mBucketNum = mCurrentBucketNum; + // Process the current bucket. if (mStarted.size() > 0) { - mDuration += (endTime - mLastStartTime); + mDuration += (current_info.mBucketEndNs - mLastStartTime); + mLastStartTime = current_info.mBucketEndNs; } - if (mDuration != 0) { - info.mDuration = mDuration; - // it will auto create new vector of CountbucketInfo if the key is not found. - mBucket.push_back(info); - VLOG(" duration: %lld", (long long)mDuration); + if (mDuration > 0) { + current_info.mDuration = mDuration; + mBucket.push_back(current_info); + addPastBucketToAnomalyTrackers(current_info.mDuration, current_info.mBucketNum); + VLOG(" duration: %lld", (long long)current_info.mDuration); } if (mStarted.size() > 0) { for (int i = 1; i < numBucketsForward; i++) { DurationBucket info; - info.mBucketStartNs = oldBucketStartTimeNs + mBucketSizeNs * i; - info.mBucketEndNs = endTime + mBucketSizeNs * i; + info.mBucketStartNs = mCurrentBucketStartTimeNs + mBucketSizeNs * i; + info.mBucketEndNs = info.mBucketStartNs + mBucketSizeNs; + info.mBucketNum = mCurrentBucketNum + i; info.mDuration = mBucketSizeNs; - mBucket.push_back(info); - VLOG(" add filling bucket with duration %lld", (long long)mBucketSizeNs); + mLastStartTime = info.mBucketEndNs; + if (info.mDuration > 0) { + mBucket.push_back(info); + addPastBucketToAnomalyTrackers(info.mDuration, info.mBucketNum); + VLOG(" add filling bucket with duration %lld", (long long)info.mDuration); + } } } - mLastStartTime = mCurrentBucketStartTimeNs; + mCurrentBucketStartTimeNs += numBucketsForward * mBucketSizeNs; + mCurrentBucketNum += numBucketsForward; + mDuration = 0; // if all stopped, then tell owner it's safe to remove this tracker. @@ -133,6 +156,8 @@ bool OringDurationTracker::flushIfNeeded(uint64_t eventTime) { } void OringDurationTracker::onSlicedConditionMayChange(const uint64_t timestamp) { + flushIfNeeded(timestamp); + declareAnomalyIfAlarmExpired(timestamp); vector> startedToPaused; vector> pausedToStarted; if (!mStarted.empty()) { @@ -154,9 +179,11 @@ void OringDurationTracker::onSlicedConditionMayChange(const uint64_t timestamp) } if (mStarted.empty()) { - mDuration += (timestamp - mLastStartTime); + mDuration = (timestamp - mLastStartTime); + mLastStartTime = -1; VLOG("Duration add %lld , to %lld ", (long long)(timestamp - mLastStartTime), (long long)mDuration); + detectAndDeclareAnomaly(timestamp, mCurrentBucketNum, mDuration); } } @@ -183,26 +210,95 @@ void OringDurationTracker::onSlicedConditionMayChange(const uint64_t timestamp) } } + if (mStarted.empty() && !pausedToStarted.empty()) { + startAnomalyAlarm(timestamp); + } mStarted.insert(pausedToStarted.begin(), pausedToStarted.end()); mPaused.insert(startedToPaused.begin(), startedToPaused.end()); + + if (mStarted.empty()) { + stopAnomalyAlarm(); + } } void OringDurationTracker::onConditionChanged(bool condition, const uint64_t timestamp) { + flushIfNeeded(timestamp); + declareAnomalyIfAlarmExpired(timestamp); if (condition) { if (!mPaused.empty()) { VLOG("Condition true, all started"); if (mStarted.empty()) { - mLastStartTime = timestamp; + mLastStartTime = -1; + } + if (mStarted.empty() && !mPaused.empty()) { + startAnomalyAlarm(timestamp); } mStarted.insert(mPaused.begin(), mPaused.end()); + mPaused.clear(); } } else { if (!mStarted.empty()) { VLOG("Condition false, all paused"); - mDuration += (timestamp - mLastStartTime); + mDuration = (timestamp - mLastStartTime); + mLastStartTime = -1; mPaused.insert(mStarted.begin(), mStarted.end()); + mStarted.clear(); + detectAndDeclareAnomaly(timestamp, mCurrentBucketNum, mDuration); } } + if (mStarted.empty()) { + stopAnomalyAlarm(); + } +} + +int64_t OringDurationTracker::predictAnomalyTimestampNs(const AnomalyTracker& anomalyTracker, + const uint64_t eventTimestampNs) const { + // TODO: Unit-test this and see if it can be done more efficiently (e.g. use int32). + // All variables below represent durations (not timestamps). + + // The time until the current bucket ends. This is how much more 'space' it can hold. + const int64_t currRemainingBucketSizeNs = + mBucketSizeNs - (eventTimestampNs - mCurrentBucketStartTimeNs); + // TODO: This should never be < 0. Document/guard against possible failures if it is. + + const int64_t thresholdNs = anomalyTracker.getAnomalyThreshold(); + + // As we move into the future, old buckets get overwritten (so their old data is erased). + + // Sum of past durations. Will change as we overwrite old buckets. + int64_t pastNs = mDuration; + pastNs += anomalyTracker.getSumOverPastBuckets(mEventKey); + + // How much of the threshold is still unaccounted after considering pastNs. + int64_t leftNs = thresholdNs - pastNs; + + // First deal with the remainder of the current bucket. + if (leftNs <= currRemainingBucketSizeNs) { // Predict the anomaly will occur in this bucket. + return eventTimestampNs + leftNs; + } + // The remainder of this bucket contributes, but we must then move to the next bucket. + pastNs += currRemainingBucketSizeNs; + + // Now deal with the past buckets, starting with the oldest. + for (int futBucketIdx = 0; futBucketIdx < anomalyTracker.getNumOfPastPackets(); + futBucketIdx++) { + // We now overwrite the oldest bucket with the previous 'current', and start a new + // 'current'. + pastNs -= anomalyTracker.getPastBucketValue( + mEventKey, mCurrentBucketNum - anomalyTracker.getNumOfPastPackets() + futBucketIdx); + leftNs = thresholdNs - pastNs; + if (leftNs <= mBucketSizeNs) { // Predict anomaly will occur in this bucket. + return eventTimestampNs + currRemainingBucketSizeNs + (futBucketIdx * mBucketSizeNs) + + leftNs; + } else { // This bucket would be entirely filled, and we'll need to move to the next + // bucket. + pastNs += mBucketSizeNs; + } + } + + // If we have reached this point, we even have to overwrite the the original current bucket. + // Thus, none of the past data will still be extant - pastNs is now 0. + return eventTimestampNs + thresholdNs; } } // namespace statsd diff --git a/cmds/statsd/src/metrics/duration_helper/OringDurationTracker.h b/cmds/statsd/src/metrics/duration_helper/OringDurationTracker.h index 78760ba1a0240..6f921139b76fa 100644 --- a/cmds/statsd/src/metrics/duration_helper/OringDurationTracker.h +++ b/cmds/statsd/src/metrics/duration_helper/OringDurationTracker.h @@ -27,8 +27,10 @@ namespace statsd { // Tracks the "Or'd" duration -- if 2 durations are overlapping, they won't be double counted. class OringDurationTracker : public DurationTracker { public: - OringDurationTracker(sp wizard, int conditionIndex, bool nesting, - uint64_t currentBucketStartNs, uint64_t bucketSizeNs, + OringDurationTracker(const HashableDimensionKey& eventKey, sp wizard, + int conditionIndex, bool nesting, uint64_t currentBucketStartNs, + uint64_t bucketSizeNs, + const std::vector>& anomalyTrackers, std::vector& bucket); void noteStart(const HashableDimensionKey& key, bool condition, const uint64_t eventTime, const ConditionKey& conditionKey) override; @@ -39,6 +41,9 @@ public: void onConditionChanged(bool condition, const uint64_t timestamp) override; bool flushIfNeeded(uint64_t timestampNs) override; + int64_t predictAnomalyTimestampNs(const AnomalyTracker& anomalyTracker, + const uint64_t currentTimestamp) const override; + private: // We don't need to keep track of individual durations. The information that's needed is: // 1) which keys are started. We record the first start time. @@ -49,6 +54,12 @@ private: std::map mPaused; int64_t mLastStartTime; std::map mConditionKeyMap; + + FRIEND_TEST(OringDurationTrackerTest, TestDurationOverlap); + FRIEND_TEST(OringDurationTrackerTest, TestCrossBucketBoundary); + FRIEND_TEST(OringDurationTrackerTest, TestDurationConditionChange); + FRIEND_TEST(OringDurationTrackerTest, TestPredictAnomalyTimestamp); + FRIEND_TEST(OringDurationTrackerTest, TestAnomalyDetection); }; } // namespace statsd diff --git a/cmds/statsd/src/metrics/metrics_manager_util.cpp b/cmds/statsd/src/metrics/metrics_manager_util.cpp index d83c144d5d8bc..2344cb4170a3e 100644 --- a/cmds/statsd/src/metrics/metrics_manager_util.cpp +++ b/cmds/statsd/src/metrics/metrics_manager_util.cpp @@ -190,7 +190,8 @@ bool initMetrics(const StatsdConfig& config, const unordered_map& l vector>& allConditionTrackers, vector>& allMetricProducers, unordered_map>& conditionToMetricMap, - unordered_map>& trackerToMetricMap) { + unordered_map>& trackerToMetricMap, + unordered_map& metricMap) { sp wizard = new ConditionWizard(allConditionTrackers); const int allMetricsCount = config.count_metric_size() + config.duration_metric_size() + config.event_metric_size() + config.value_metric_size(); @@ -208,6 +209,7 @@ bool initMetrics(const StatsdConfig& config, const unordered_map& l } int metricIndex = allMetricProducers.size(); + metricMap.insert({metric.name(), metricIndex}); int trackerIndex; if (!handleMetricWithLogTrackers(metric.what(), metricIndex, metric.dimension_size() > 0, allLogEntryMatchers, logTrackerMap, trackerToMetricMap, @@ -236,6 +238,7 @@ bool initMetrics(const StatsdConfig& config, const unordered_map& l for (int i = 0; i < config.duration_metric_size(); i++) { int metricIndex = allMetricProducers.size(); const DurationMetric& metric = config.duration_metric(i); + metricMap.insert({metric.name(), metricIndex}); auto what_it = conditionTrackerMap.find(metric.what()); if (what_it == conditionTrackerMap.end()) { @@ -305,6 +308,7 @@ bool initMetrics(const StatsdConfig& config, const unordered_map& l for (int i = 0; i < config.event_metric_size(); i++) { int metricIndex = allMetricProducers.size(); const EventMetric& metric = config.event_metric(i); + metricMap.insert({metric.name(), metricIndex}); if (!metric.has_name() || !metric.has_what()) { ALOGW("cannot find the metric name or what in config"); return false; @@ -342,6 +346,7 @@ bool initMetrics(const StatsdConfig& config, const unordered_map& l } int metricIndex = allMetricProducers.size(); + metricMap.insert({metric.name(), metricIndex}); int trackerIndex; if (!handleMetricWithLogTrackers(metric.what(), metricIndex, metric.dimension_size() > 0, allLogEntryMatchers, logTrackerMap, trackerToMetricMap, @@ -387,6 +392,7 @@ bool initMetrics(const StatsdConfig& config, const unordered_map& l } int metricIndex = allMetricProducers.size(); + metricMap.insert({metric.name(), metricIndex}); int trackerIndex; if (!handleMetricWithLogTrackers(metric.what(), metricIndex, metric.dimension_size() > 0, allLogEntryMatchers, logTrackerMap, trackerToMetricMap, @@ -419,21 +425,43 @@ bool initMetrics(const StatsdConfig& config, const unordered_map& l } sp gaugeProducer = - new GaugeMetricProducer(metric, conditionIndex, wizard, pullTagId); + new GaugeMetricProducer(metric, conditionIndex, wizard, pullTagId, startTimeNs); allMetricProducers.push_back(gaugeProducer); } return true; } +bool initAlerts(const StatsdConfig& config, const unordered_map& metricProducerMap, + vector>& allMetricProducers, + vector>& allAnomalyTrackers) { + for (int i = 0; i < config.alert_size(); i++) { + const Alert& alert = config.alert(i); + const auto& itr = metricProducerMap.find(alert.metric_name()); + if (itr == metricProducerMap.end()) { + ALOGW("alert has unknown metric name: %s %s", alert.name().c_str(), + alert.metric_name().c_str()); + return false; + } + const int metricIndex = itr->second; + sp anomalyTracker = + new AnomalyTracker(alert, allMetricProducers[metricIndex]->getBuckeSizeInNs()); + allMetricProducers[metricIndex]->addAnomalyTracker(anomalyTracker); + allAnomalyTrackers.push_back(anomalyTracker); + } + return true; +} + bool initStatsdConfig(const StatsdConfig& config, set& allTagIds, vector>& allLogEntryMatchers, vector>& allConditionTrackers, vector>& allMetricProducers, + vector>& allAnomalyTrackers, unordered_map>& conditionToMetricMap, unordered_map>& trackerToMetricMap, unordered_map>& trackerToConditionMap) { unordered_map logTrackerMap; unordered_map conditionTrackerMap; + unordered_map metricProducerMap; if (!initLogTrackers(config, logTrackerMap, allLogEntryMatchers, allTagIds)) { ALOGE("initLogMatchingTrackers failed"); @@ -449,10 +477,14 @@ bool initStatsdConfig(const StatsdConfig& config, set& allTagIds, if (!initMetrics(config, logTrackerMap, conditionTrackerMap, allLogEntryMatchers, allConditionTrackers, allMetricProducers, conditionToMetricMap, - trackerToMetricMap)) { + trackerToMetricMap, metricProducerMap)) { ALOGE("initMetricProducers failed"); return false; } + if (!initAlerts(config, metricProducerMap, allMetricProducers, allAnomalyTrackers)) { + ALOGE("initAlerts failed"); + return false; + } return true; } diff --git a/cmds/statsd/src/metrics/metrics_manager_util.h b/cmds/statsd/src/metrics/metrics_manager_util.h index edf3af0aff8e6..7d7e0c36951f1 100644 --- a/cmds/statsd/src/metrics/metrics_manager_util.h +++ b/cmds/statsd/src/metrics/metrics_manager_util.h @@ -88,6 +88,7 @@ bool initStatsdConfig(const StatsdConfig& config, std::set& allTagIds, std::vector>& allLogEntryMatchers, std::vector>& allConditionTrackers, std::vector>& allMetricProducers, + vector>& allAnomalyTrackers, std::unordered_map>& conditionToMetricMap, std::unordered_map>& trackerToMetricMap, std::unordered_map>& trackerToConditionMap); diff --git a/cmds/statsd/src/stats_util.h b/cmds/statsd/src/stats_util.h index e1d0aceb3bd5e..b7d8f97afaae0 100644 --- a/cmds/statsd/src/stats_util.h +++ b/cmds/statsd/src/stats_util.h @@ -35,15 +35,14 @@ typedef std::string HashableDimensionKey; typedef std::map ConditionKey; -// TODO: For P, change int to int64_t. -// TODO: Should HashableDimensionKey be marked here as const? -typedef std::unordered_map DimToValMap; +typedef std::unordered_map DimToValMap; EventMetricData parse(log_msg msg); int getTagId(log_msg msg); std::string getHashableKey(std::vector key); + } // namespace statsd } // namespace os } // namespace android diff --git a/cmds/statsd/src/statsd_config.proto b/cmds/statsd/src/statsd_config.proto index d3b04ba5e32d4..e75a37f98966f 100644 --- a/cmds/statsd/src/statsd_config.proto +++ b/cmds/statsd/src/statsd_config.proto @@ -113,11 +113,11 @@ message Bucket { } message EventConditionLink { - optional string condition = 1; + optional string condition = 1; - repeated KeyMatcher key_in_main = 2; + repeated KeyMatcher key_in_main = 2; - repeated KeyMatcher key_in_condition = 3; + repeated KeyMatcher key_in_condition = 3; } message EventMetric { @@ -217,8 +217,6 @@ message Alert { optional int32 refractory_period_secs = 5; optional int64 trigger_if_sum_gt = 6; - - optional int32 refractory_period_in_buckets = 7; } message StatsdConfig { diff --git a/cmds/statsd/tests/AnomalyMonitor_test.cpp b/cmds/statsd/tests/AnomalyMonitor_test.cpp index 59fa16007eef3..920ca08ef9558 100644 --- a/cmds/statsd/tests/AnomalyMonitor_test.cpp +++ b/cmds/statsd/tests/AnomalyMonitor_test.cpp @@ -20,6 +20,8 @@ using namespace android::os::statsd; #ifdef __ANDROID__ TEST(AnomalyMonitor, popSoonerThan) { + std::string emptyMetricId; + std::string emptyDimensionId; unordered_set, SpHash> set; AnomalyMonitor am(2); diff --git a/cmds/statsd/tests/MetricsManager_test.cpp b/cmds/statsd/tests/MetricsManager_test.cpp index caa1cf4dba1be..3dd4e70ff49c7 100644 --- a/cmds/statsd/tests/MetricsManager_test.cpp +++ b/cmds/statsd/tests/MetricsManager_test.cpp @@ -34,6 +34,7 @@ using android::sp; using std::set; using std::unordered_map; using std::vector; +using android::os::statsd::Condition; #ifdef __ANDROID__ @@ -71,6 +72,19 @@ StatsdConfig buildGoodConfig() { combination->add_matcher("SCREEN_IS_ON"); combination->add_matcher("SCREEN_IS_OFF"); + CountMetric* metric = config.add_count_metric(); + metric->set_name("3"); + metric->set_what("SCREEN_IS_ON"); + metric->mutable_bucket()->set_bucket_size_millis(30 * 1000L); + KeyMatcher* keyMatcher = metric->add_dimension(); + keyMatcher->set_key(1); + + auto alert = config.add_alert(); + alert->set_name("3"); + alert->set_metric_name("3"); + alert->set_number_of_buckets(10); + alert->set_refractory_period_secs(100); + alert->set_trigger_if_sum_gt(100); return config; } @@ -100,6 +114,29 @@ StatsdConfig buildCircleMatchers() { return config; } +StatsdConfig buildAlertWithUnknownMetric() { + StatsdConfig config; + config.set_name("12345"); + + LogEntryMatcher* eventMatcher = config.add_log_entry_matcher(); + eventMatcher->set_name("SCREEN_IS_ON"); + + CountMetric* metric = config.add_count_metric(); + metric->set_name("3"); + metric->set_what("SCREEN_IS_ON"); + metric->mutable_bucket()->set_bucket_size_millis(30 * 1000L); + KeyMatcher* keyMatcher = metric->add_dimension(); + keyMatcher->set_key(1); + + auto alert = config.add_alert(); + alert->set_name("3"); + alert->set_metric_name("2"); + alert->set_number_of_buckets(10); + alert->set_refractory_period_secs(100); + alert->set_trigger_if_sum_gt(100); + return config; +} + StatsdConfig buildMissingMatchers() { StatsdConfig config; config.set_name("12345"); @@ -156,6 +193,12 @@ StatsdConfig buildDimensionMetricsWithMultiTags() { KeyMatcher* keyMatcher = metric->add_dimension(); keyMatcher->set_key(1); + auto alert = config.add_alert(); + alert->set_name("3"); + alert->set_metric_name("3"); + alert->set_number_of_buckets(10); + alert->set_refractory_period_secs(100); + alert->set_trigger_if_sum_gt(100); return config; } @@ -183,7 +226,7 @@ StatsdConfig buildCircleConditions() { simpleLogEntryMatcher->mutable_key_value_matcher(0)->set_eq_int( 1 /*SCREEN_STATE_CHANGE__DISPLAY_STATE__STATE_OFF*/); - Condition* condition = config.add_condition(); + auto condition = config.add_condition(); condition->set_name("SCREEN_IS_ON"); SimpleCondition* simpleCondition = condition->mutable_simple_condition(); simpleCondition->set_start("SCREEN_IS_ON"); @@ -206,13 +249,16 @@ TEST(MetricsManagerTest, TestGoodConfig) { vector> allLogEntryMatchers; vector> allConditionTrackers; vector> allMetricProducers; + std::vector> allAnomalyTrackers; unordered_map> conditionToMetricMap; unordered_map> trackerToMetricMap; unordered_map> trackerToConditionMap; EXPECT_TRUE(initStatsdConfig(config, allTagIds, allLogEntryMatchers, allConditionTrackers, - allMetricProducers, conditionToMetricMap, trackerToMetricMap, - trackerToConditionMap)); + allMetricProducers, allAnomalyTrackers, conditionToMetricMap, + trackerToMetricMap, trackerToConditionMap)); + EXPECT_EQ(1u, allMetricProducers.size()); + EXPECT_EQ(1u, allAnomalyTrackers.size()); } TEST(MetricsManagerTest, TestDimensionMetricsWithMultiTags) { @@ -221,13 +267,14 @@ TEST(MetricsManagerTest, TestDimensionMetricsWithMultiTags) { vector> allLogEntryMatchers; vector> allConditionTrackers; vector> allMetricProducers; + std::vector> allAnomalyTrackers; unordered_map> conditionToMetricMap; unordered_map> trackerToMetricMap; unordered_map> trackerToConditionMap; EXPECT_FALSE(initStatsdConfig(config, allTagIds, allLogEntryMatchers, allConditionTrackers, - allMetricProducers, conditionToMetricMap, trackerToMetricMap, - trackerToConditionMap)); + allMetricProducers, allAnomalyTrackers, conditionToMetricMap, + trackerToMetricMap, trackerToConditionMap)); } TEST(MetricsManagerTest, TestCircleLogMatcherDependency) { @@ -236,13 +283,14 @@ TEST(MetricsManagerTest, TestCircleLogMatcherDependency) { vector> allLogEntryMatchers; vector> allConditionTrackers; vector> allMetricProducers; + std::vector> allAnomalyTrackers; unordered_map> conditionToMetricMap; unordered_map> trackerToMetricMap; unordered_map> trackerToConditionMap; EXPECT_FALSE(initStatsdConfig(config, allTagIds, allLogEntryMatchers, allConditionTrackers, - allMetricProducers, conditionToMetricMap, trackerToMetricMap, - trackerToConditionMap)); + allMetricProducers, allAnomalyTrackers, conditionToMetricMap, + trackerToMetricMap, trackerToConditionMap)); } TEST(MetricsManagerTest, TestMissingMatchers) { @@ -251,13 +299,13 @@ TEST(MetricsManagerTest, TestMissingMatchers) { vector> allLogEntryMatchers; vector> allConditionTrackers; vector> allMetricProducers; + std::vector> allAnomalyTrackers; unordered_map> conditionToMetricMap; unordered_map> trackerToMetricMap; unordered_map> trackerToConditionMap; - EXPECT_FALSE(initStatsdConfig(config, allTagIds, allLogEntryMatchers, allConditionTrackers, - allMetricProducers, conditionToMetricMap, trackerToMetricMap, - trackerToConditionMap)); + allMetricProducers, allAnomalyTrackers, conditionToMetricMap, + trackerToMetricMap, trackerToConditionMap)); } TEST(MetricsManagerTest, TestCircleConditionDependency) { @@ -266,13 +314,30 @@ TEST(MetricsManagerTest, TestCircleConditionDependency) { vector> allLogEntryMatchers; vector> allConditionTrackers; vector> allMetricProducers; + std::vector> allAnomalyTrackers; unordered_map> conditionToMetricMap; unordered_map> trackerToMetricMap; unordered_map> trackerToConditionMap; EXPECT_FALSE(initStatsdConfig(config, allTagIds, allLogEntryMatchers, allConditionTrackers, - allMetricProducers, conditionToMetricMap, trackerToMetricMap, - trackerToConditionMap)); + allMetricProducers, allAnomalyTrackers, conditionToMetricMap, + trackerToMetricMap, trackerToConditionMap)); +} + +TEST(MetricsManagerTest, testAlertWithUnknownMetric) { + StatsdConfig config = buildAlertWithUnknownMetric(); + set allTagIds; + vector> allLogEntryMatchers; + vector> allConditionTrackers; + vector> allMetricProducers; + std::vector> allAnomalyTrackers; + unordered_map> conditionToMetricMap; + unordered_map> trackerToMetricMap; + unordered_map> trackerToConditionMap; + + EXPECT_FALSE(initStatsdConfig(config, allTagIds, allLogEntryMatchers, allConditionTrackers, + allMetricProducers, allAnomalyTrackers, conditionToMetricMap, + trackerToMetricMap, trackerToConditionMap)); } #else diff --git a/cmds/statsd/tests/UidMap_test.cpp b/cmds/statsd/tests/UidMap_test.cpp index 4c12b0397769e..0c194683884cc 100644 --- a/cmds/statsd/tests/UidMap_test.cpp +++ b/cmds/statsd/tests/UidMap_test.cpp @@ -34,7 +34,8 @@ const string kApp2 = "app2.sharing.1"; TEST(UidMapTest, TestIsolatedUID) { sp m = new UidMap(); - StatsLogProcessor p(m, nullptr); + sp anomalyMonitor; + StatsLogProcessor p(m, anomalyMonitor, nullptr); LogEvent addEvent(android::util::ISOLATED_UID_CHANGED, 1); addEvent.write(100); // parent UID addEvent.write(101); // isolated UID diff --git a/cmds/statsd/tests/anomaly/AnomalyTracker_test.cpp b/cmds/statsd/tests/anomaly/AnomalyTracker_test.cpp index b8150d04ed079..e0200f27316e0 100644 --- a/cmds/statsd/tests/anomaly/AnomalyTracker_test.cpp +++ b/cmds/statsd/tests/anomaly/AnomalyTracker_test.cpp @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "src/anomaly/DiscreteAnomalyTracker.h" +#include "src/anomaly/AnomalyTracker.h" #include #include @@ -37,7 +37,7 @@ void AddValueToBucket(const std::vector>& key_value_pair } } -std::shared_ptr MockeBucket( +std::shared_ptr MockBucket( const std::vector>& key_value_pair_list) { std::shared_ptr bucket = std::make_shared(); AddValueToBucket(key_value_pair_list, bucket); @@ -45,190 +45,240 @@ std::shared_ptr MockeBucket( } TEST(AnomalyTrackerTest, TestConsecutiveBuckets) { + const int64_t bucketSizeNs = 30 * NS_PER_SEC; Alert alert; alert.set_number_of_buckets(3); - alert.set_refractory_period_in_buckets(3); + alert.set_refractory_period_secs(2 * bucketSizeNs / NS_PER_SEC); alert.set_trigger_if_sum_gt(2); - DiscreteAnomalyTracker anomaly_tracker(alert); + AnomalyTracker anomalyTracker(alert, bucketSizeNs); - std::shared_ptr bucket0 = MockeBucket({{"a", 1}, {"b", 2}, {"c", 1}}); - // Adds bucket #0 - anomaly_tracker.addOrUpdateBucket(bucket0, 0); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.size(), 3UL); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("a")->second, 1); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("b")->second, 2); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("c")->second, 1); - EXPECT_FALSE(anomaly_tracker.detectAnomaly()); - anomaly_tracker.declareAndDeclareAnomaly(); - EXPECT_EQ(anomaly_tracker.mAnomalyDeclared, 0L); + std::shared_ptr bucket0 = MockBucket({{"a", 1}, {"b", 2}, {"c", 1}}); + int64_t eventTimestamp0 = 10; + std::shared_ptr bucket1 = MockBucket({{"a", 1}}); + int64_t eventTimestamp1 = bucketSizeNs + 11; + std::shared_ptr bucket2 = MockBucket({{"b", 1}}); + int64_t eventTimestamp2 = 2 * bucketSizeNs + 12; + std::shared_ptr bucket3 = MockBucket({{"a", 2}}); + int64_t eventTimestamp3 = 3 * bucketSizeNs + 13; + std::shared_ptr bucket4 = MockBucket({{"b", 1}}); + int64_t eventTimestamp4 = 4 * bucketSizeNs + 14; + std::shared_ptr bucket5 = MockBucket({{"a", 2}}); + int64_t eventTimestamp5 = 5 * bucketSizeNs + 15; + std::shared_ptr bucket6 = MockBucket({{"a", 2}}); + int64_t eventTimestamp6 = 6 * bucketSizeNs + 16; - // Adds bucket #0 again. The sum does not change. - anomaly_tracker.addOrUpdateBucket(bucket0, 0); - EXPECT_EQ(anomaly_tracker.mCurrentBucketIndex, 0L); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.size(), 3UL); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("a")->second, 1); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("b")->second, 2); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("c")->second, 1); - EXPECT_FALSE(anomaly_tracker.detectAnomaly()); - anomaly_tracker.declareAndDeclareAnomaly(); - EXPECT_EQ(anomaly_tracker.mAnomalyDeclared, 0L); - EXPECT_EQ(anomaly_tracker.mLastAlarmAtBucketIndex, -1L); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 0u); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, -1LL); + EXPECT_FALSE(anomalyTracker.detectAnomaly(0, *bucket0)); + anomalyTracker.detectAndDeclareAnomaly(eventTimestamp0, 0, *bucket0); + EXPECT_EQ(anomalyTracker.mLastAlarmTimestampNs, -1L); - // Adds bucket #1. - std::shared_ptr bucket1 = MockeBucket({{"b", 2}}); - anomaly_tracker.addOrUpdateBucket(bucket1, 1); - EXPECT_EQ(anomaly_tracker.mCurrentBucketIndex, 1L); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.size(), 3UL); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("a")->second, 1); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("b")->second, 4); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("c")->second, 1); - // Alarm. - EXPECT_TRUE(anomaly_tracker.detectAnomaly()); - anomaly_tracker.declareAndDeclareAnomaly(); - EXPECT_EQ(anomaly_tracker.mAnomalyDeclared, 1L); - EXPECT_EQ(anomaly_tracker.mLastAlarmAtBucketIndex, 1L); + // Adds past bucket #0 + anomalyTracker.addPastBucket(bucket0, 0); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 3u); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("a"), 1LL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("b"), 2LL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("c"), 1LL); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, 0LL); + EXPECT_FALSE(anomalyTracker.detectAnomaly(1, *bucket1)); + anomalyTracker.detectAndDeclareAnomaly(eventTimestamp1, 1, *bucket1); + EXPECT_EQ(anomalyTracker.mLastAlarmTimestampNs, -1L); - // Adds bucket #1 again. The sum does not change. - anomaly_tracker.addOrUpdateBucket(bucket1, 1); - EXPECT_EQ(anomaly_tracker.mCurrentBucketIndex, 1L); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.size(), 3UL); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("a")->second, 1); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("b")->second, 4); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("c")->second, 1); - // Alarm. - EXPECT_TRUE(anomaly_tracker.detectAnomaly()); - anomaly_tracker.declareAndDeclareAnomaly(); - EXPECT_EQ(anomaly_tracker.mAnomalyDeclared, 1L); - EXPECT_EQ(anomaly_tracker.mLastAlarmAtBucketIndex, 1L); + // Adds past bucket #0 again. The sum does not change. + anomalyTracker.addPastBucket(bucket0, 0); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 3u); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("a"), 1LL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("b"), 2LL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("c"), 1LL); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, 0LL); + EXPECT_FALSE(anomalyTracker.detectAnomaly(1, *bucket1)); + anomalyTracker.detectAndDeclareAnomaly(eventTimestamp1 + 1, 1, *bucket1); + EXPECT_EQ(anomalyTracker.mLastAlarmTimestampNs, -1L); - // Adds bucket #2. - anomaly_tracker.addOrUpdateBucket(MockeBucket({{"a", 1}}), 2); - EXPECT_EQ(anomaly_tracker.mCurrentBucketIndex, 2L); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.size(), 3UL); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("a")->second, 2); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("b")->second, 4); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("c")->second, 1); - EXPECT_TRUE(anomaly_tracker.detectAnomaly()); + // Adds past bucket #1. + anomalyTracker.addPastBucket(bucket1, 1); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, 1L); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 3UL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("a"), 2LL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("b"), 2LL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("c"), 1LL); + EXPECT_TRUE(anomalyTracker.detectAnomaly(2, *bucket2)); + anomalyTracker.detectAndDeclareAnomaly(eventTimestamp2, 2, *bucket2); + EXPECT_EQ(anomalyTracker.mLastAlarmTimestampNs, eventTimestamp2); + + // Adds past bucket #1 again. Nothing changes. + anomalyTracker.addPastBucket(bucket1, 1); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, 1L); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 3UL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("a"), 2LL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("b"), 2LL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("c"), 1LL); + EXPECT_TRUE(anomalyTracker.detectAnomaly(2, *bucket2)); + anomalyTracker.detectAndDeclareAnomaly(eventTimestamp2 + 1, 2, *bucket2); + EXPECT_EQ(anomalyTracker.mLastAlarmTimestampNs, eventTimestamp2); + + // Adds past bucket #2. + anomalyTracker.addPastBucket(bucket2, 2); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, 2L); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 2UL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("a"), 1LL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("b"), 1LL); + EXPECT_TRUE(anomalyTracker.detectAnomaly(3, *bucket3)); + anomalyTracker.detectAndDeclareAnomaly(eventTimestamp3, 3, *bucket3); // Within refractory period. - anomaly_tracker.declareAndDeclareAnomaly(); - EXPECT_EQ(anomaly_tracker.mAnomalyDeclared, 1L); - EXPECT_EQ(anomaly_tracker.mLastAlarmAtBucketIndex, 1L); + EXPECT_EQ(anomalyTracker.mLastAlarmTimestampNs, eventTimestamp2); // Adds bucket #3. - anomaly_tracker.addOrUpdateBucket(MockeBucket({{"a", 1}}), 3); - EXPECT_EQ(anomaly_tracker.mCurrentBucketIndex, 3L); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.size(), 2UL); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("a")->second, 2); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("b")->second, 2); - EXPECT_FALSE(anomaly_tracker.detectAnomaly()); + anomalyTracker.addPastBucket(bucket3, 3L); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, 3L); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 2UL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("a"), 2LL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("b"), 1LL); + EXPECT_FALSE(anomalyTracker.detectAnomaly(4, *bucket4)); + anomalyTracker.detectAndDeclareAnomaly(eventTimestamp4, 4, *bucket4); + EXPECT_EQ(anomalyTracker.mLastAlarmTimestampNs, eventTimestamp2); - // Adds bucket #3. - anomaly_tracker.addOrUpdateBucket(MockeBucket({{"a", 2}}), 4); - EXPECT_EQ(anomaly_tracker.mCurrentBucketIndex, 4L); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.size(), 1UL); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("a")->second, 4); - EXPECT_TRUE(anomaly_tracker.detectAnomaly()); - // Within refractory period. - anomaly_tracker.declareAndDeclareAnomaly(); - EXPECT_EQ(anomaly_tracker.mAnomalyDeclared, 1L); - EXPECT_EQ(anomaly_tracker.mLastAlarmAtBucketIndex, 1L); + // Adds bucket #4. + anomalyTracker.addPastBucket(bucket4, 4); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, 4L); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 2UL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("a"), 2LL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("b"), 1LL); + EXPECT_TRUE(anomalyTracker.detectAnomaly(5, *bucket5)); + anomalyTracker.detectAndDeclareAnomaly(eventTimestamp5, 5, *bucket5); + EXPECT_EQ(anomalyTracker.mLastAlarmTimestampNs, eventTimestamp5); - anomaly_tracker.addOrUpdateBucket(MockeBucket({{"a", 1}}), 5); - EXPECT_EQ(anomaly_tracker.mCurrentBucketIndex, 5L); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.size(), 1UL); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("a")->second, 4); - EXPECT_TRUE(anomaly_tracker.detectAnomaly()); + // Adds bucket #5. + anomalyTracker.addPastBucket(bucket5, 5); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, 5L); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 2UL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("a"), 2LL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("b"), 1LL); + EXPECT_TRUE(anomalyTracker.detectAnomaly(6, *bucket6)); // Within refractory period. - anomaly_tracker.declareAndDeclareAnomaly(); - EXPECT_EQ(anomaly_tracker.mAnomalyDeclared, 2L); - EXPECT_EQ(anomaly_tracker.mLastAlarmAtBucketIndex, 5L); + anomalyTracker.detectAndDeclareAnomaly(eventTimestamp6, 6, *bucket6); + EXPECT_EQ(anomalyTracker.mLastAlarmTimestampNs, eventTimestamp5); } TEST(AnomalyTrackerTest, TestSparseBuckets) { + const int64_t bucketSizeNs = 30 * NS_PER_SEC; Alert alert; alert.set_number_of_buckets(3); - alert.set_refractory_period_in_buckets(3); + alert.set_refractory_period_secs(2 * bucketSizeNs / NS_PER_SEC); alert.set_trigger_if_sum_gt(2); - DiscreteAnomalyTracker anomaly_tracker(alert); + AnomalyTracker anomalyTracker(alert, bucketSizeNs); - // Add bucket #9 - anomaly_tracker.addOrUpdateBucket(MockeBucket({{"a", 1}, {"b", 2}, {"c", 1}}), 9); - EXPECT_EQ(anomaly_tracker.mCurrentBucketIndex, 9L); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.size(), 3UL); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("a")->second, 1); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("b")->second, 2); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("c")->second, 1); - EXPECT_FALSE(anomaly_tracker.detectAnomaly()); - anomaly_tracker.declareAndDeclareAnomaly(); - EXPECT_EQ(anomaly_tracker.mAnomalyDeclared, 0L); - EXPECT_EQ(anomaly_tracker.mLastAlarmAtBucketIndex, -1L); + std::shared_ptr bucket9 = MockBucket({{"a", 1}, {"b", 2}, {"c", 1}}); + std::shared_ptr bucket16 = MockBucket({{"b", 4}}); + std::shared_ptr bucket18 = MockBucket({{"b", 1}, {"c", 1}}); + std::shared_ptr bucket20 = MockBucket({{"b", 3}, {"c", 1}}); + std::shared_ptr bucket25 = MockBucket({{"d", 1}}); + std::shared_ptr bucket28 = MockBucket({{"e", 2}}); - // Add bucket #16 - anomaly_tracker.addOrUpdateBucket(MockeBucket({{"b", 4}}), 16); - EXPECT_EQ(anomaly_tracker.mCurrentBucketIndex, 16L); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.size(), 1UL); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("b")->second, 4); - EXPECT_TRUE(anomaly_tracker.detectAnomaly()); - anomaly_tracker.declareAndDeclareAnomaly(); - EXPECT_EQ(anomaly_tracker.mAnomalyDeclared, 1L); - EXPECT_EQ(anomaly_tracker.mLastAlarmAtBucketIndex, 16L); + int64_t eventTimestamp1 = bucketSizeNs * 8 + 1; + int64_t eventTimestamp2 = bucketSizeNs * 15 + 11; + int64_t eventTimestamp3 = bucketSizeNs * 17 + 1; + int64_t eventTimestamp4 = bucketSizeNs * 19 + 2; + int64_t eventTimestamp5 = bucketSizeNs * 24 + 3; + int64_t eventTimestamp6 = bucketSizeNs * 27 + 3; - // Add bucket #18 - anomaly_tracker.addOrUpdateBucket(MockeBucket({{"b", 1}, {"c", 1}}), 18); - EXPECT_EQ(anomaly_tracker.mCurrentBucketIndex, 18L); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.size(), 2UL); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("b")->second, 5); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("c")->second, 1); - EXPECT_TRUE(anomaly_tracker.detectAnomaly()); - anomaly_tracker.declareAndDeclareAnomaly(); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, -1LL); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 0UL); + EXPECT_FALSE(anomalyTracker.detectAnomaly(9, *bucket9)); + anomalyTracker.detectAndDeclareAnomaly(eventTimestamp1, 9, *bucket9); + EXPECT_EQ(anomalyTracker.mLastAlarmTimestampNs, -1); + + // Add past bucket #9 + anomalyTracker.addPastBucket(bucket9, 9); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, 9L); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 3UL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("a"), 1LL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("b"), 2LL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("c"), 1LL); + EXPECT_TRUE(anomalyTracker.detectAnomaly(16, *bucket16)); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 0UL); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, 15L); + anomalyTracker.detectAndDeclareAnomaly(eventTimestamp2, 16, *bucket16); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 0UL); + EXPECT_EQ(anomalyTracker.mLastAlarmTimestampNs, eventTimestamp2); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, 15L); + + // Add past bucket #16 + anomalyTracker.addPastBucket(bucket16, 16); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, 16L); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 1UL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("b"), 4LL); + EXPECT_TRUE(anomalyTracker.detectAnomaly(18, *bucket18)); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 1UL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("b"), 4LL); // Within refractory period. - EXPECT_EQ(anomaly_tracker.mAnomalyDeclared, 1L); - EXPECT_EQ(anomaly_tracker.mLastAlarmAtBucketIndex, 16L); + anomalyTracker.detectAndDeclareAnomaly(eventTimestamp3, 18, *bucket18); + EXPECT_EQ(anomalyTracker.mLastAlarmTimestampNs, eventTimestamp2); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 1UL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("b"), 4LL); - // Add bucket #18 again. - anomaly_tracker.addOrUpdateBucket(MockeBucket({{"b", 1}, {"c", 1}}), 18); - EXPECT_EQ(anomaly_tracker.mCurrentBucketIndex, 18L); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.size(), 2UL); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("b")->second, 5); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("c")->second, 1); - EXPECT_TRUE(anomaly_tracker.detectAnomaly()); - anomaly_tracker.declareAndDeclareAnomaly(); - EXPECT_EQ(anomaly_tracker.mAnomalyDeclared, 1L); - EXPECT_EQ(anomaly_tracker.mLastAlarmAtBucketIndex, 16L); + // Add past bucket #18 + anomalyTracker.addPastBucket(bucket18, 18); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, 18L); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 2UL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("b"), 1LL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("c"), 1LL); + EXPECT_TRUE(anomalyTracker.detectAnomaly(20, *bucket20)); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, 19L); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 2UL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("b"), 1LL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("c"), 1LL); + anomalyTracker.detectAndDeclareAnomaly(eventTimestamp4, 20, *bucket20); + EXPECT_EQ(anomalyTracker.mLastAlarmTimestampNs, eventTimestamp4); - // Add bucket #20 - anomaly_tracker.addOrUpdateBucket(MockeBucket({{"b", 3}, {"d", 1}}), 20); - EXPECT_EQ(anomaly_tracker.mCurrentBucketIndex, 20L); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.size(), 3UL); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("b")->second, 4); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("c")->second, 1); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("d")->second, 1); - EXPECT_TRUE(anomaly_tracker.detectAnomaly()); - anomaly_tracker.declareAndDeclareAnomaly(); - EXPECT_EQ(anomaly_tracker.mAnomalyDeclared, 2L); - EXPECT_EQ(anomaly_tracker.mLastAlarmAtBucketIndex, 20L); + // Add bucket #18 again. Nothing changes. + anomalyTracker.addPastBucket(bucket18, 18); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, 19L); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 2UL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("b"), 1LL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("c"), 1LL); + EXPECT_TRUE(anomalyTracker.detectAnomaly(20, *bucket20)); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 2UL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("b"), 1LL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("c"), 1LL); + anomalyTracker.detectAndDeclareAnomaly(eventTimestamp4 + 1, 20, *bucket20); + // Within refractory period. + EXPECT_EQ(anomalyTracker.mLastAlarmTimestampNs, eventTimestamp4); - // Add bucket #25 - anomaly_tracker.addOrUpdateBucket(MockeBucket({{"d", 1}}), 25); - EXPECT_EQ(anomaly_tracker.mCurrentBucketIndex, 25L); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.size(), 1UL); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("d")->second, 1L); - EXPECT_FALSE(anomaly_tracker.detectAnomaly()); - anomaly_tracker.declareAndDeclareAnomaly(); - EXPECT_EQ(anomaly_tracker.mAnomalyDeclared, 2L); - EXPECT_EQ(anomaly_tracker.mLastAlarmAtBucketIndex, 20L); + // Add past bucket #20 + anomalyTracker.addPastBucket(bucket20, 20); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, 20L); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 2UL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("b"), 3LL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("c"), 1LL); + EXPECT_FALSE(anomalyTracker.detectAnomaly(25, *bucket25)); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, 24L); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 0UL); + anomalyTracker.detectAndDeclareAnomaly(eventTimestamp5, 25, *bucket25); + EXPECT_EQ(anomalyTracker.mLastAlarmTimestampNs, eventTimestamp4); - // Add bucket #28 - anomaly_tracker.addOrUpdateBucket(MockeBucket({{"e", 5}}), 28); - EXPECT_EQ(anomaly_tracker.mCurrentBucketIndex, 28L); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.size(), 1UL); - EXPECT_EQ(anomaly_tracker.mSumOverPastBuckets.find("e")->second, 5L); - EXPECT_TRUE(anomaly_tracker.detectAnomaly()); - anomaly_tracker.declareAndDeclareAnomaly(); - EXPECT_EQ(anomaly_tracker.mAnomalyDeclared, 3L); - EXPECT_EQ(anomaly_tracker.mLastAlarmAtBucketIndex, 28L); + // Add past bucket #25 + anomalyTracker.addPastBucket(bucket25, 25); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, 25L); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 1UL); + EXPECT_EQ(anomalyTracker.getSumOverPastBuckets("d"), 1LL); + EXPECT_FALSE(anomalyTracker.detectAnomaly(28, *bucket28)); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, 27L); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 0UL); + anomalyTracker.detectAndDeclareAnomaly(eventTimestamp6, 28, *bucket28); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 0UL); + EXPECT_EQ(anomalyTracker.mLastAlarmTimestampNs, eventTimestamp4); + + // Updates current bucket #28. + (*bucket28)["e"] = 5; + EXPECT_TRUE(anomalyTracker.detectAnomaly(28, *bucket28)); + EXPECT_EQ(anomalyTracker.mMostRecentBucketNum, 27L); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 0UL); + anomalyTracker.detectAndDeclareAnomaly(eventTimestamp6 + 7, 28, *bucket28); + EXPECT_EQ(anomalyTracker.mSumOverPastBuckets.size(), 0UL); + EXPECT_EQ(anomalyTracker.mLastAlarmTimestampNs, eventTimestamp6 + 7); } } // namespace statsd diff --git a/cmds/statsd/tests/indexed_priority_queue_test.cpp b/cmds/statsd/tests/indexed_priority_queue_test.cpp index 600b95356083f..d6cd876e5ad53 100644 --- a/cmds/statsd/tests/indexed_priority_queue_test.cpp +++ b/cmds/statsd/tests/indexed_priority_queue_test.cpp @@ -22,10 +22,12 @@ using namespace android::os::statsd; /** struct for template in indexed_priority_queue */ struct AATest : public RefBase { - AATest(uint32_t val) : val(val) { + AATest(uint32_t val, std::string a, std::string b) : val(val), a(a), b(b) { } const int val; + const std::string a; + const std::string b; struct Smaller { bool operator()(const sp a, const sp b) const { @@ -36,9 +38,11 @@ struct AATest : public RefBase { #ifdef __ANDROID__ TEST(indexed_priority_queue, empty_and_size) { + std::string emptyMetricId; + std::string emptyDimensionId; indexed_priority_queue ipq; - sp aa4 = new AATest{4}; - sp aa8 = new AATest{8}; + sp aa4 = new AATest{4, emptyMetricId, emptyDimensionId}; + sp aa8 = new AATest{8, emptyMetricId, emptyDimensionId}; EXPECT_EQ(0u, ipq.size()); EXPECT_TRUE(ipq.empty()); @@ -61,13 +65,15 @@ TEST(indexed_priority_queue, empty_and_size) { } TEST(indexed_priority_queue, top) { + std::string emptyMetricId; + std::string emptyDimensionId; indexed_priority_queue ipq; - sp aa2 = new AATest{2}; - sp aa4 = new AATest{4}; - sp aa8 = new AATest{8}; - sp aa12 = new AATest{12}; - sp aa16 = new AATest{16}; - sp aa20 = new AATest{20}; + sp aa2 = new AATest{2, emptyMetricId, emptyDimensionId}; + sp aa4 = new AATest{4, emptyMetricId, emptyDimensionId}; + sp aa8 = new AATest{8, emptyMetricId, emptyDimensionId}; + sp aa12 = new AATest{12, emptyMetricId, emptyDimensionId}; + sp aa16 = new AATest{16, emptyMetricId, emptyDimensionId}; + sp aa20 = new AATest{20, emptyMetricId, emptyDimensionId}; EXPECT_EQ(ipq.top(), nullptr); @@ -113,9 +119,11 @@ TEST(indexed_priority_queue, top) { } TEST(indexed_priority_queue, push_same_aa) { + std::string emptyMetricId; + std::string emptyDimensionId; indexed_priority_queue ipq; - sp aa4_a = new AATest{4}; - sp aa4_b = new AATest{4}; + sp aa4_a = new AATest{4, emptyMetricId, emptyDimensionId}; + sp aa4_b = new AATest{4, emptyMetricId, emptyDimensionId}; ipq.push(aa4_a); EXPECT_EQ(1u, ipq.size()); @@ -134,9 +142,11 @@ TEST(indexed_priority_queue, push_same_aa) { } TEST(indexed_priority_queue, remove_nonexistant) { + std::string emptyMetricId; + std::string emptyDimensionId; indexed_priority_queue ipq; - sp aa4 = new AATest{4}; - sp aa5 = new AATest{5}; + sp aa4 = new AATest{4, emptyMetricId, emptyDimensionId}; + sp aa5 = new AATest{5, emptyMetricId, emptyDimensionId}; ipq.push(aa4); ipq.remove(aa5); @@ -147,8 +157,10 @@ TEST(indexed_priority_queue, remove_nonexistant) { TEST(indexed_priority_queue, remove_same_aa) { indexed_priority_queue ipq; - sp aa4_a = new AATest{4}; - sp aa4_b = new AATest{4}; + std::string emptyMetricId; + std::string emptyDimensionId; + sp aa4_a = new AATest{4, emptyMetricId, emptyDimensionId}; + sp aa4_b = new AATest{4, emptyMetricId, emptyDimensionId}; ipq.push(aa4_a); ipq.push(aa4_b); @@ -184,9 +196,11 @@ TEST(indexed_priority_queue, nulls) { TEST(indexed_priority_queue, pop) { indexed_priority_queue ipq; - sp a = new AATest{1}; - sp b = new AATest{2}; - sp c = new AATest{3}; + std::string emptyMetricId; + std::string emptyDimensionId; + sp a = new AATest{1, emptyMetricId, emptyDimensionId}; + sp b = new AATest{2, emptyMetricId, emptyDimensionId}; + sp c = new AATest{3, emptyMetricId, emptyDimensionId}; ipq.push(c); ipq.push(b); diff --git a/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp b/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp index b7c9b406e0b92..35e08af687396 100644 --- a/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp +++ b/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp @@ -54,21 +54,26 @@ TEST(CountMetricProducerTest, TestNonDimensionalEvents) { // 2 events in bucket 1. countProducer.onMatchedLogEvent(1 /*log matcher index*/, event1, false); countProducer.onMatchedLogEvent(1 /*log matcher index*/, event2, false); - countProducer.flushCounterIfNeeded(bucketStartTimeNs + bucketSizeNs + 1); + + // Flushes at event #2. + countProducer.flushIfNeeded(bucketStartTimeNs + 2); + EXPECT_EQ(0UL, countProducer.mPastBuckets.size()); + + // Flushes. + countProducer.flushIfNeeded(bucketStartTimeNs + bucketSizeNs + 1); EXPECT_EQ(1UL, countProducer.mPastBuckets.size()); EXPECT_TRUE(countProducer.mPastBuckets.find(DEFAULT_DIMENSION_KEY) != countProducer.mPastBuckets.end()); const auto& buckets = countProducer.mPastBuckets[DEFAULT_DIMENSION_KEY]; EXPECT_EQ(1UL, buckets.size()); - const auto& bucketInfo = buckets[0]; - EXPECT_EQ(bucketStartTimeNs, bucketInfo.mBucketStartNs); - EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, bucketInfo.mBucketEndNs); - EXPECT_EQ(2LL, bucketInfo.mCount); + EXPECT_EQ(bucketStartTimeNs, buckets[0].mBucketStartNs); + EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, buckets[0].mBucketEndNs); + EXPECT_EQ(2LL, buckets[0].mCount); // 1 matched event happens in bucket 2. LogEvent event3(tagId, bucketStartTimeNs + bucketSizeNs + 2); countProducer.onMatchedLogEvent(1 /*log matcher index*/, event3, false); - countProducer.flushCounterIfNeeded(bucketStartTimeNs + 2 * bucketSizeNs + 1); + countProducer.flushIfNeeded(bucketStartTimeNs + 2 * bucketSizeNs + 1); EXPECT_EQ(1UL, countProducer.mPastBuckets.size()); EXPECT_TRUE(countProducer.mPastBuckets.find(DEFAULT_DIMENSION_KEY) != countProducer.mPastBuckets.end()); @@ -79,7 +84,7 @@ TEST(CountMetricProducerTest, TestNonDimensionalEvents) { EXPECT_EQ(1LL, bucketInfo2.mCount); // nothing happens in bucket 3. we should not record anything for bucket 3. - countProducer.flushCounterIfNeeded(bucketStartTimeNs + 3 * bucketSizeNs + 1); + countProducer.flushIfNeeded(bucketStartTimeNs + 3 * bucketSizeNs + 1); EXPECT_EQ(1UL, countProducer.mPastBuckets.size()); EXPECT_TRUE(countProducer.mPastBuckets.find(DEFAULT_DIMENSION_KEY) != countProducer.mPastBuckets.end()); @@ -108,20 +113,22 @@ TEST(CountMetricProducerTest, TestEventsWithNonSlicedCondition) { EXPECT_EQ(0UL, countProducer.mPastBuckets.size()); countProducer.onConditionChanged(false /*new condition*/, bucketStartTimeNs + 2); + // Upon this match event, the matched event1 is flushed. countProducer.onMatchedLogEvent(1 /*matcher index*/, event2, false /*pulled*/); EXPECT_EQ(0UL, countProducer.mPastBuckets.size()); - countProducer.flushCounterIfNeeded(bucketStartTimeNs + bucketSizeNs + 1); - + countProducer.flushIfNeeded(bucketStartTimeNs + bucketSizeNs + 1); EXPECT_EQ(1UL, countProducer.mPastBuckets.size()); EXPECT_TRUE(countProducer.mPastBuckets.find(DEFAULT_DIMENSION_KEY) != countProducer.mPastBuckets.end()); - const auto& buckets = countProducer.mPastBuckets[DEFAULT_DIMENSION_KEY]; - EXPECT_EQ(1UL, buckets.size()); - const auto& bucketInfo = buckets[0]; - EXPECT_EQ(bucketStartTimeNs, bucketInfo.mBucketStartNs); - EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, bucketInfo.mBucketEndNs); - EXPECT_EQ(1LL, bucketInfo.mCount); + { + const auto& buckets = countProducer.mPastBuckets[DEFAULT_DIMENSION_KEY]; + EXPECT_EQ(1UL, buckets.size()); + const auto& bucketInfo = buckets[0]; + EXPECT_EQ(bucketStartTimeNs, bucketInfo.mBucketStartNs); + EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, bucketInfo.mBucketEndNs); + EXPECT_EQ(1LL, bucketInfo.mCount); + } } TEST(CountMetricProducerTest, TestEventsWithSlicedCondition) { @@ -158,10 +165,11 @@ TEST(CountMetricProducerTest, TestEventsWithSlicedCondition) { bucketStartTimeNs); countProducer.onMatchedLogEvent(1 /*log matcher index*/, event1, false); + countProducer.flushIfNeeded(bucketStartTimeNs + 1); + EXPECT_EQ(0UL, countProducer.mPastBuckets.size()); + countProducer.onMatchedLogEvent(1 /*log matcher index*/, event2, false); - - countProducer.flushCounterIfNeeded(bucketStartTimeNs + bucketSizeNs + 1); - + countProducer.flushIfNeeded(bucketStartTimeNs + bucketSizeNs + 1); EXPECT_EQ(1UL, countProducer.mPastBuckets.size()); EXPECT_TRUE(countProducer.mPastBuckets.find(DEFAULT_DIMENSION_KEY) != countProducer.mPastBuckets.end()); @@ -173,6 +181,68 @@ TEST(CountMetricProducerTest, TestEventsWithSlicedCondition) { EXPECT_EQ(1LL, bucketInfo.mCount); } +TEST(CountMetricProducerTest, TestAnomalyDetection) { + Alert alert; + alert.set_name("alert"); + alert.set_metric_name("1"); + alert.set_trigger_if_sum_gt(2); + alert.set_number_of_buckets(2); + alert.set_refractory_period_secs(1); + + int64_t bucketStartTimeNs = 10000000000; + int64_t bucketSizeNs = 30 * NS_PER_SEC; + int64_t bucket2StartTimeNs = bucketStartTimeNs + bucketSizeNs; + int64_t bucket3StartTimeNs = bucketStartTimeNs + 2 * bucketSizeNs; + + sp anomalyTracker = new AnomalyTracker(alert, bucketSizeNs); + + CountMetric metric; + metric.set_name("1"); + metric.mutable_bucket()->set_bucket_size_millis(bucketSizeNs / 1000000); + + sp wizard = new NaggyMock(); + CountMetricProducer countProducer(metric, -1 /*-1 meaning no condition*/, wizard, + bucketStartTimeNs); + countProducer.addAnomalyTracker(anomalyTracker); + + int tagId = 1; + LogEvent event1(tagId, bucketStartTimeNs + 1); + LogEvent event2(tagId, bucketStartTimeNs + 2); + LogEvent event3(tagId, bucketStartTimeNs + 2 * bucketSizeNs + 1); + LogEvent event4(tagId, bucketStartTimeNs + 3 * bucketSizeNs + 1); + LogEvent event5(tagId, bucketStartTimeNs + 3 * bucketSizeNs + 2); + LogEvent event6(tagId, bucketStartTimeNs + 3 * bucketSizeNs + 3); + LogEvent event7(tagId, bucketStartTimeNs + 3 * bucketSizeNs + 3 + NS_PER_SEC); + + // Two events in bucket #0. + countProducer.onMatchedLogEvent(1 /*log matcher index*/, event1, false); + countProducer.onMatchedLogEvent(1 /*log matcher index*/, event2, false); + + EXPECT_EQ(1UL, countProducer.mCurrentSlicedCounter->size()); + EXPECT_EQ(2L, countProducer.mCurrentSlicedCounter->begin()->second); + EXPECT_EQ(anomalyTracker->getLastAlarmTimestampNs(), -1LL); + + // One event in bucket #2. No alarm as bucket #0 is trashed out. + countProducer.onMatchedLogEvent(1 /*log matcher index*/, event3, false); + EXPECT_EQ(1UL, countProducer.mCurrentSlicedCounter->size()); + EXPECT_EQ(1L, countProducer.mCurrentSlicedCounter->begin()->second); + EXPECT_EQ(anomalyTracker->getLastAlarmTimestampNs(), -1LL); + + // Two events in bucket #3. + countProducer.onMatchedLogEvent(1 /*log matcher index*/, event4, false); + countProducer.onMatchedLogEvent(1 /*log matcher index*/, event5, false); + countProducer.onMatchedLogEvent(1 /*log matcher index*/, event6, false); + EXPECT_EQ(1UL, countProducer.mCurrentSlicedCounter->size()); + EXPECT_EQ(3L, countProducer.mCurrentSlicedCounter->begin()->second); + // Anomaly at event 6 is within refractory period. The alarm is at event 5 timestamp not event 6 + EXPECT_EQ(anomalyTracker->getLastAlarmTimestampNs(), (long long)event5.GetTimestampNs()); + + countProducer.onMatchedLogEvent(1 /*log matcher index*/, event7, false); + EXPECT_EQ(1UL, countProducer.mCurrentSlicedCounter->size()); + EXPECT_EQ(4L, countProducer.mCurrentSlicedCounter->begin()->second); + EXPECT_EQ(anomalyTracker->getLastAlarmTimestampNs(), (long long)event7.GetTimestampNs()); +} + } // namespace statsd } // namespace os } // namespace android diff --git a/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp b/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp new file mode 100644 index 0000000000000..b9e2b8a7752f8 --- /dev/null +++ b/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp @@ -0,0 +1,201 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "logd/LogEvent.h" +#include "metrics_test_helper.h" +#include "src/metrics/GaugeMetricProducer.h" + +#include +#include +#include +#include + +using namespace testing; +using android::sp; +using std::set; +using std::unordered_map; +using std::vector; + +#ifdef __ANDROID__ + +namespace android { +namespace os { +namespace statsd { + +TEST(GaugeMetricProducerTest, TestWithCondition) { + int64_t bucketStartTimeNs = 10000000000; + int64_t bucketSizeNs = 60 * 1000 * 1000 * 1000LL; + + GaugeMetric metric; + metric.set_name("1"); + metric.mutable_bucket()->set_bucket_size_millis(bucketSizeNs / 1000000); + metric.set_gauge_field(2); + + sp wizard = new NaggyMock(); + + GaugeMetricProducer gaugeProducer(metric, 1 /*has condition*/, wizard, -1, bucketStartTimeNs); + + vector> allData; + std::shared_ptr event1 = std::make_shared(1, bucketStartTimeNs + 1); + event1->write(1); + event1->write(13); + event1->init(); + allData.push_back(event1); + + std::shared_ptr event2 = std::make_shared(1, bucketStartTimeNs + 10); + event2->write(1); + event2->write(15); + event2->init(); + allData.push_back(event2); + + gaugeProducer.onDataPulled(allData); + gaugeProducer.flushIfNeeded(event2->GetTimestampNs() + 1); + EXPECT_EQ(0UL, gaugeProducer.mCurrentSlicedBucket->size()); + EXPECT_EQ(0UL, gaugeProducer.mPastBuckets.size()); + + gaugeProducer.onConditionChanged(true, bucketStartTimeNs + 11); + gaugeProducer.onConditionChanged(false, bucketStartTimeNs + 21); + gaugeProducer.onConditionChanged(true, bucketStartTimeNs + bucketSizeNs + 11); + std::shared_ptr event3 = + std::make_shared(1, bucketStartTimeNs + 2 * bucketSizeNs + 10); + event3->write(1); + event3->write(25); + event3->init(); + allData.push_back(event3); + gaugeProducer.onDataPulled(allData); + gaugeProducer.flushIfNeeded(bucketStartTimeNs + 2 * bucketSizeNs + 10); + EXPECT_EQ(1UL, gaugeProducer.mCurrentSlicedBucket->size()); + EXPECT_EQ(25L, gaugeProducer.mCurrentSlicedBucket->begin()->second); + // One dimension. + EXPECT_EQ(1UL, gaugeProducer.mPastBuckets.size()); + EXPECT_EQ(1UL, gaugeProducer.mPastBuckets.begin()->second.size()); + EXPECT_EQ(25L, gaugeProducer.mPastBuckets.begin()->second.front().mGauge); + EXPECT_EQ(2UL, gaugeProducer.mPastBuckets.begin()->second.front().mBucketNum); + EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs, + gaugeProducer.mPastBuckets.begin()->second.front().mBucketStartNs); +} + +TEST(GaugeMetricProducerTest, TestNoCondition) { + int64_t bucketStartTimeNs = 10000000000; + int64_t bucketSizeNs = 60 * 1000 * 1000 * 1000LL; + + GaugeMetric metric; + metric.set_name("1"); + metric.mutable_bucket()->set_bucket_size_millis(bucketSizeNs / 1000000); + metric.set_gauge_field(2); + + sp wizard = new NaggyMock(); + + GaugeMetricProducer gaugeProducer(metric, -1 /*no condition*/, wizard, -1, bucketStartTimeNs); + + vector> allData; + std::shared_ptr event1 = std::make_shared(1, bucketStartTimeNs + 1); + event1->write(1); + event1->write(13); + event1->init(); + allData.push_back(event1); + + std::shared_ptr event2 = std::make_shared(1, bucketStartTimeNs + 10); + event2->write(1); + event2->write(15); + event2->init(); + allData.push_back(event2); + + std::shared_ptr event3 = + std::make_shared(1, bucketStartTimeNs + 2 * bucketSizeNs + 10); + event3->write(1); + event3->write(25); + event3->init(); + allData.push_back(event3); + + gaugeProducer.onDataPulled(allData); + // Has one slice + EXPECT_EQ(1UL, gaugeProducer.mCurrentSlicedBucket->size()); + EXPECT_EQ(25L, gaugeProducer.mCurrentSlicedBucket->begin()->second); + EXPECT_EQ(2UL, gaugeProducer.mPastBuckets.begin()->second.size()); + EXPECT_EQ(13L, gaugeProducer.mPastBuckets.begin()->second.front().mGauge); + EXPECT_EQ(0UL, gaugeProducer.mPastBuckets.begin()->second.front().mBucketNum); + EXPECT_EQ(25L, gaugeProducer.mPastBuckets.begin()->second.back().mGauge); + EXPECT_EQ(2UL, gaugeProducer.mPastBuckets.begin()->second.back().mBucketNum); + EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs, + gaugeProducer.mPastBuckets.begin()->second.back().mBucketStartNs); +} + +TEST(GaugeMetricProducerTest, TestAnomalyDetection) { + int64_t bucketStartTimeNs = 10000000000; + int64_t bucketSizeNs = 60 * 1000 * 1000 * 1000LL; + sp wizard = new NaggyMock(); + + GaugeMetric metric; + metric.set_name("1"); + metric.mutable_bucket()->set_bucket_size_millis(bucketSizeNs / 1000000); + metric.set_gauge_field(2); + GaugeMetricProducer gaugeProducer(metric, -1 /*no condition*/, wizard, -1, bucketStartTimeNs); + + Alert alert; + alert.set_name("alert"); + alert.set_metric_name("1"); + alert.set_trigger_if_sum_gt(25); + alert.set_number_of_buckets(2); + sp anomalyTracker = new AnomalyTracker(alert, bucketSizeNs); + gaugeProducer.addAnomalyTracker(anomalyTracker); + + std::shared_ptr event1 = std::make_shared(1, bucketStartTimeNs + 1); + event1->write(1); + event1->write(13); + event1->init(); + + gaugeProducer.onDataPulled({event1}); + EXPECT_EQ(1UL, gaugeProducer.mCurrentSlicedBucket->size()); + EXPECT_EQ(13L, gaugeProducer.mCurrentSlicedBucket->begin()->second); + EXPECT_EQ(anomalyTracker->getLastAlarmTimestampNs(), -1LL); + + std::shared_ptr event2 = + std::make_shared(1, bucketStartTimeNs + bucketSizeNs + 10); + event2->write(1); + event2->write(15); + event2->init(); + + gaugeProducer.onDataPulled({event2}); + EXPECT_EQ(1UL, gaugeProducer.mCurrentSlicedBucket->size()); + EXPECT_EQ(15L, gaugeProducer.mCurrentSlicedBucket->begin()->second); + EXPECT_EQ(anomalyTracker->getLastAlarmTimestampNs(), (long long)event2->GetTimestampNs()); + + std::shared_ptr event3 = + std::make_shared(1, bucketStartTimeNs + 2 * bucketSizeNs + 10); + event3->write(1); + event3->write(24); + event3->init(); + + gaugeProducer.onDataPulled({event3}); + EXPECT_EQ(1UL, gaugeProducer.mCurrentSlicedBucket->size()); + EXPECT_EQ(24L, gaugeProducer.mCurrentSlicedBucket->begin()->second); + EXPECT_EQ(anomalyTracker->getLastAlarmTimestampNs(), (long long)event3->GetTimestampNs()); + + // The event4 does not have the gauge field. Thus the current bucket value is 0. + std::shared_ptr event4 = + std::make_shared(1, bucketStartTimeNs + 3 * bucketSizeNs + 10); + event4->write(1); + event4->init(); + gaugeProducer.onDataPulled({event4}); + EXPECT_EQ(0UL, gaugeProducer.mCurrentSlicedBucket->size()); + EXPECT_EQ(anomalyTracker->getLastAlarmTimestampNs(), (long long)event3->GetTimestampNs()); +} + +} // namespace statsd +} // namespace os +} // namespace android +#else +GTEST_LOG_(INFO) << "This test does nothing.\n"; +#endif diff --git a/cmds/statsd/tests/metrics/MaxDurationTracker_test.cpp b/cmds/statsd/tests/metrics/MaxDurationTracker_test.cpp index 9cc184a1a435c..9e169bbf10efa 100644 --- a/cmds/statsd/tests/metrics/MaxDurationTracker_test.cpp +++ b/cmds/statsd/tests/metrics/MaxDurationTracker_test.cpp @@ -45,17 +45,49 @@ TEST(MaxDurationTrackerTest, TestSimpleMaxDuration) { uint64_t bucketStartTimeNs = 10000000000; uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL; - MaxDurationTracker tracker(wizard, -1, false, bucketStartTimeNs, bucketSizeNs, buckets); + MaxDurationTracker tracker("event", wizard, -1, false, bucketStartTimeNs, bucketSizeNs, {}, + buckets); - tracker.noteStart("", true, bucketStartTimeNs, key1); - tracker.noteStop("", bucketStartTimeNs + 10, false); + tracker.noteStart("1", true, bucketStartTimeNs, key1); + // Event starts again. This would not change anything as it already starts. + tracker.noteStart("1", true, bucketStartTimeNs + 3, key1); + // Stopped. + tracker.noteStop("1", bucketStartTimeNs + 10, false); - tracker.noteStart("", true, bucketStartTimeNs + 20, key1); - tracker.noteStop("", bucketStartTimeNs + 40, false); + // Another event starts in this bucket. + tracker.noteStart("2", true, bucketStartTimeNs + 20, key1); + tracker.noteStop("2", bucketStartTimeNs + 40, false /*stop all*/); tracker.flushIfNeeded(bucketStartTimeNs + bucketSizeNs + 1); EXPECT_EQ(1u, buckets.size()); - EXPECT_EQ(20, buckets[0].mDuration); + EXPECT_EQ(20ULL, buckets[0].mDuration); +} + +TEST(MaxDurationTrackerTest, TestStopAll) { + sp wizard = new NaggyMock(); + + vector buckets; + ConditionKey key1; + + uint64_t bucketStartTimeNs = 10000000000; + uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL; + + MaxDurationTracker tracker("event", wizard, -1, false, bucketStartTimeNs, bucketSizeNs, {}, + buckets); + + tracker.noteStart("1", true, bucketStartTimeNs + 1, key1); + + // Another event starts in this bucket. + tracker.noteStart("2", true, bucketStartTimeNs + 20, key1); + tracker.noteStopAll(bucketStartTimeNs + bucketSizeNs + 40); + EXPECT_TRUE(tracker.mInfos.empty()); + EXPECT_EQ(1u, buckets.size()); + EXPECT_EQ(bucketSizeNs - 1, buckets[0].mDuration); + + tracker.flushIfNeeded(bucketStartTimeNs + 3 * bucketSizeNs + 40); + EXPECT_EQ(2u, buckets.size()); + EXPECT_EQ(bucketSizeNs - 1, buckets[0].mDuration); + EXPECT_EQ(40ULL, buckets[1].mDuration); } TEST(MaxDurationTrackerTest, TestCrossBucketBoundary) { @@ -67,14 +99,33 @@ TEST(MaxDurationTrackerTest, TestCrossBucketBoundary) { uint64_t bucketStartTimeNs = 10000000000; uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL; - MaxDurationTracker tracker(wizard, -1, false, bucketStartTimeNs, bucketSizeNs, buckets); + MaxDurationTracker tracker("event", wizard, -1, false, bucketStartTimeNs, bucketSizeNs, {}, + buckets); + // The event starts. tracker.noteStart("", true, bucketStartTimeNs + 1, key1); - tracker.flushIfNeeded(bucketStartTimeNs + (2 * bucketSizeNs) + 1); + // Starts again. Does not change anything. + tracker.noteStart("", true, bucketStartTimeNs + bucketSizeNs + 1, key1); + + // Flushes at early 2nd bucket. The event still does not stop yet. + tracker.flushIfNeeded(bucketStartTimeNs + bucketSizeNs + 1); + EXPECT_EQ(1u, buckets.size()); + EXPECT_EQ((unsigned long long)(bucketSizeNs - 1), buckets[0].mDuration); + + // Flushes at the end of the 2nd bucket. The event still does not stop yet. + tracker.flushIfNeeded(bucketStartTimeNs + (2 * bucketSizeNs)); EXPECT_EQ(2u, buckets.size()); - EXPECT_EQ((long long)(bucketSizeNs - 1), buckets[0].mDuration); - EXPECT_EQ((long long)bucketSizeNs, buckets[1].mDuration); + EXPECT_EQ((unsigned long long)(bucketSizeNs - 1), buckets[0].mDuration); + EXPECT_EQ((unsigned long long)bucketSizeNs, buckets[1].mDuration); + + // The event stops at early 4th bucket. + tracker.noteStop("", bucketStartTimeNs + (3 * bucketSizeNs) + 20, false /*stop all*/); + tracker.flushIfNeeded(bucketStartTimeNs + (3 * bucketSizeNs) + 21); + EXPECT_EQ(3u, buckets.size()); + EXPECT_EQ((unsigned long long)(bucketSizeNs - 1), buckets[0].mDuration); + EXPECT_EQ((unsigned long long)bucketSizeNs, buckets[1].mDuration); + EXPECT_EQ((unsigned long long)bucketSizeNs, buckets[2].mDuration); } TEST(MaxDurationTrackerTest, TestCrossBucketBoundary_nested) { @@ -86,7 +137,8 @@ TEST(MaxDurationTrackerTest, TestCrossBucketBoundary_nested) { uint64_t bucketStartTimeNs = 10000000000; uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL; - MaxDurationTracker tracker(wizard, -1, true, bucketStartTimeNs, bucketSizeNs, buckets); + MaxDurationTracker tracker("event", wizard, -1, true, bucketStartTimeNs, bucketSizeNs, {}, + buckets); // 2 starts tracker.noteStart("", true, bucketStartTimeNs + 1, key1); @@ -97,15 +149,17 @@ TEST(MaxDurationTrackerTest, TestCrossBucketBoundary_nested) { tracker.flushIfNeeded(bucketStartTimeNs + (2 * bucketSizeNs) + 1); EXPECT_EQ(2u, buckets.size()); - EXPECT_EQ((long long)(bucketSizeNs - 1), buckets[0].mDuration); - EXPECT_EQ((long long)bucketSizeNs, buckets[1].mDuration); + EXPECT_EQ(bucketSizeNs - 1, buckets[0].mDuration); + EXPECT_EQ(bucketSizeNs, buckets[1].mDuration); // real stop now. tracker.noteStop("", bucketStartTimeNs + (2 * bucketSizeNs) + 5, false); tracker.flushIfNeeded(bucketStartTimeNs + (3 * bucketSizeNs) + 1); EXPECT_EQ(3u, buckets.size()); - EXPECT_EQ(5, buckets[2].mDuration); + EXPECT_EQ(bucketSizeNs - 1, buckets[0].mDuration); + EXPECT_EQ(bucketSizeNs, buckets[1].mDuration); + EXPECT_EQ(5ULL, buckets[2].mDuration); } TEST(MaxDurationTrackerTest, TestMaxDurationWithCondition) { @@ -124,17 +178,65 @@ TEST(MaxDurationTrackerTest, TestMaxDurationWithCondition) { uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL; int64_t durationTimeNs = 2 * 1000; - MaxDurationTracker tracker(wizard, 1, false, bucketStartTimeNs, bucketSizeNs, buckets); + MaxDurationTracker tracker("event", wizard, 1, false, bucketStartTimeNs, bucketSizeNs, {}, + buckets); + EXPECT_TRUE(tracker.mAnomalyTrackers.empty()); tracker.noteStart("2:maps", true, eventStartTimeNs, key1); + tracker.onSlicedConditionMayChange(eventStartTimeNs + 2 * bucketSizeNs + 5); + EXPECT_EQ(2u, buckets.size()); + EXPECT_EQ(bucketSizeNs - 1, buckets[0].mDuration); + EXPECT_EQ(bucketSizeNs, buckets[1].mDuration); - tracker.onSlicedConditionMayChange(eventStartTimeNs + 5); + tracker.noteStop("2:maps", eventStartTimeNs + 2 * bucketSizeNs + durationTimeNs, false); + EXPECT_EQ(2u, buckets.size()); + EXPECT_EQ(bucketSizeNs - 1, buckets[0].mDuration); + EXPECT_EQ(bucketSizeNs, buckets[1].mDuration); + EXPECT_TRUE(tracker.mInfos.empty()); + EXPECT_EQ(6LL, tracker.mDuration); - tracker.noteStop("2:maps", eventStartTimeNs + durationTimeNs, false); + tracker.noteStart("2:maps", false, eventStartTimeNs + 3 * bucketSizeNs + 10, key1); + EXPECT_EQ(1u, tracker.mInfos.size()); + for (const auto& itr : tracker.mInfos) { + EXPECT_EQ(DurationState::kPaused, itr.second.state); + EXPECT_EQ(0LL, itr.second.lastDuration); + } + EXPECT_EQ(3u, buckets.size()); + EXPECT_EQ(bucketSizeNs - 1, buckets[0].mDuration); + EXPECT_EQ(bucketSizeNs, buckets[1].mDuration); + EXPECT_EQ(6ULL, buckets[2].mDuration); +} - tracker.flushIfNeeded(bucketStartTimeNs + bucketSizeNs + 1); - EXPECT_EQ(1u, buckets.size()); - EXPECT_EQ(5, buckets[0].mDuration); +TEST(MaxDurationTrackerTest, TestAnomalyDetection) { + Alert alert; + alert.set_name("alert"); + alert.set_metric_name("1"); + alert.set_trigger_if_sum_gt(32 * NS_PER_SEC); + alert.set_number_of_buckets(2); + alert.set_refractory_period_secs(1); + + vector buckets; + sp wizard = new NaggyMock(); + ConditionKey key1; + key1["APP_BACKGROUND"] = "1:maps|"; + uint64_t bucketStartTimeNs = 10 * NS_PER_SEC; + uint64_t eventStartTimeNs = bucketStartTimeNs + NS_PER_SEC + 1; + uint64_t bucketSizeNs = 30 * NS_PER_SEC; + + sp anomalyTracker = new AnomalyTracker(alert, bucketSizeNs); + MaxDurationTracker tracker("event", wizard, -1, true, bucketStartTimeNs, bucketSizeNs, + {anomalyTracker}, buckets); + + tracker.noteStart("1", true, eventStartTimeNs, key1); + tracker.noteStop("1", eventStartTimeNs + 10, false); + EXPECT_EQ(anomalyTracker->mLastAlarmTimestampNs, -1); + EXPECT_EQ(10LL, tracker.mDuration); + + tracker.noteStart("2", true, eventStartTimeNs + 20, key1); + tracker.noteStop("2", eventStartTimeNs + 2 * bucketSizeNs + 3 * NS_PER_SEC, false); + EXPECT_EQ((long long)(4 * NS_PER_SEC + 1LL), tracker.mDuration); + EXPECT_EQ(anomalyTracker->mLastAlarmTimestampNs, + (long long)(eventStartTimeNs + 2 * bucketSizeNs + 3 * NS_PER_SEC)); } } // namespace statsd diff --git a/cmds/statsd/tests/metrics/OringDurationTracker_test.cpp b/cmds/statsd/tests/metrics/OringDurationTracker_test.cpp index f495d6bc24b82..f4edffd1af1ef 100644 --- a/cmds/statsd/tests/metrics/OringDurationTracker_test.cpp +++ b/cmds/statsd/tests/metrics/OringDurationTracker_test.cpp @@ -45,16 +45,18 @@ TEST(OringDurationTrackerTest, TestDurationOverlap) { uint64_t bucketStartTimeNs = 10000000000; uint64_t eventStartTimeNs = bucketStartTimeNs + 1; uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL; - int64_t durationTimeNs = 2 * 1000; + uint64_t durationTimeNs = 2 * 1000; - OringDurationTracker tracker(wizard, 1, false, bucketStartTimeNs, bucketSizeNs, buckets); + OringDurationTracker tracker("event", wizard, 1, false, bucketStartTimeNs, bucketSizeNs, {}, + buckets); tracker.noteStart("2:maps", true, eventStartTimeNs, key1); + EXPECT_EQ((long long)eventStartTimeNs, tracker.mLastStartTime); tracker.noteStart("2:maps", true, eventStartTimeNs + 10, key1); // overlapping wl + EXPECT_EQ((long long)eventStartTimeNs, tracker.mLastStartTime); tracker.noteStop("2:maps", eventStartTimeNs + durationTimeNs, false); - - tracker.flushIfNeeded(bucketStartTimeNs + bucketSizeNs + 1); + tracker.flushIfNeeded(eventStartTimeNs + bucketSizeNs + 1); EXPECT_EQ(1u, buckets.size()); EXPECT_EQ(durationTimeNs, buckets[0].mDuration); } @@ -71,7 +73,8 @@ TEST(OringDurationTrackerTest, TestDurationNested) { uint64_t eventStartTimeNs = bucketStartTimeNs + 1; uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL; - OringDurationTracker tracker(wizard, 1, true, bucketStartTimeNs, bucketSizeNs, buckets); + OringDurationTracker tracker("event", wizard, 1, true, bucketStartTimeNs, bucketSizeNs, {}, + buckets); tracker.noteStart("2:maps", true, eventStartTimeNs, key1); tracker.noteStart("2:maps", true, eventStartTimeNs + 10, key1); // overlapping wl @@ -81,7 +84,66 @@ TEST(OringDurationTrackerTest, TestDurationNested) { tracker.flushIfNeeded(bucketStartTimeNs + bucketSizeNs + 1); EXPECT_EQ(1u, buckets.size()); - EXPECT_EQ(2003, buckets[0].mDuration); + EXPECT_EQ(2003ULL, buckets[0].mDuration); +} + +TEST(OringDurationTrackerTest, TestStopAll) { + sp wizard = new NaggyMock(); + + ConditionKey key1; + key1["APP_BACKGROUND"] = "1:maps|"; + + vector buckets; + + uint64_t bucketStartTimeNs = 10000000000; + uint64_t eventStartTimeNs = bucketStartTimeNs + 1; + uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL; + + OringDurationTracker tracker("event", wizard, 1, true, bucketStartTimeNs, bucketSizeNs, {}, + buckets); + + tracker.noteStart("2:maps", true, eventStartTimeNs, key1); + tracker.noteStart("3:maps", true, eventStartTimeNs + 10, key1); // overlapping wl + + tracker.noteStopAll(eventStartTimeNs + 2003); + + tracker.flushIfNeeded(bucketStartTimeNs + bucketSizeNs + 1); + EXPECT_EQ(1u, buckets.size()); + EXPECT_EQ(2003ULL, buckets[0].mDuration); +} + +TEST(OringDurationTrackerTest, TestCrossBucketBoundary) { + sp wizard = new NaggyMock(); + + ConditionKey key1; + key1["APP_BACKGROUND"] = "1:maps|"; + + vector buckets; + + uint64_t bucketStartTimeNs = 10000000000; + uint64_t eventStartTimeNs = bucketStartTimeNs + 1; + uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL; + uint64_t durationTimeNs = 2 * 1000; + + OringDurationTracker tracker("event", wizard, 1, true, bucketStartTimeNs, bucketSizeNs, {}, + buckets); + + tracker.noteStart("2:maps", true, eventStartTimeNs, key1); + EXPECT_EQ((long long)eventStartTimeNs, tracker.mLastStartTime); + tracker.noteStart("2:maps", true, eventStartTimeNs + 2 * bucketSizeNs, key1); + EXPECT_EQ((long long)(bucketStartTimeNs + 2 * bucketSizeNs), tracker.mLastStartTime); + + tracker.flushIfNeeded(eventStartTimeNs + 2 * bucketSizeNs); + EXPECT_EQ(2u, buckets.size()); + EXPECT_EQ(bucketSizeNs - 1, buckets[0].mDuration); + EXPECT_EQ(bucketSizeNs, buckets[1].mDuration); + + tracker.noteStop("2:maps", eventStartTimeNs + 2 * bucketSizeNs + 10, false); + tracker.noteStop("2:maps", eventStartTimeNs + 2 * bucketSizeNs + 12, false); + tracker.flushIfNeeded(eventStartTimeNs + 2 * bucketSizeNs + 12); + EXPECT_EQ(2u, buckets.size()); + EXPECT_EQ(bucketSizeNs - 1, buckets[0].mDuration); + EXPECT_EQ(bucketSizeNs, buckets[1].mDuration); } TEST(OringDurationTrackerTest, TestDurationConditionChange) { @@ -98,19 +160,19 @@ TEST(OringDurationTrackerTest, TestDurationConditionChange) { uint64_t bucketStartTimeNs = 10000000000; uint64_t eventStartTimeNs = bucketStartTimeNs + 1; uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL; - int64_t durationTimeNs = 2 * 1000; + uint64_t durationTimeNs = 2 * 1000; - OringDurationTracker tracker(wizard, 1, false, bucketStartTimeNs, bucketSizeNs, buckets); + OringDurationTracker tracker("event", wizard, 1, false, bucketStartTimeNs, bucketSizeNs, {}, + buckets); tracker.noteStart("2:maps", true, eventStartTimeNs, key1); - tracker.onSlicedConditionMayChange(eventStartTimeNs + 5); - - tracker.noteStop("2:maps", eventStartTimeNs + durationTimeNs, false); - - tracker.flushIfNeeded(bucketStartTimeNs + bucketSizeNs + 1); - EXPECT_EQ(1u, buckets.size()); - EXPECT_EQ(5, buckets[0].mDuration); + tracker.onSlicedConditionMayChange(eventStartTimeNs + 2 * bucketSizeNs + 5); + tracker.noteStop("2:maps", eventStartTimeNs + 2 * bucketSizeNs + durationTimeNs, false); + tracker.flushIfNeeded(bucketStartTimeNs + 2 * bucketSizeNs + durationTimeNs); + EXPECT_EQ(2u, buckets.size()); + EXPECT_EQ(bucketSizeNs - 1, buckets[0].mDuration); + EXPECT_EQ(bucketSizeNs, buckets[1].mDuration); } TEST(OringDurationTrackerTest, TestDurationConditionChangeNested) { @@ -128,7 +190,8 @@ TEST(OringDurationTrackerTest, TestDurationConditionChangeNested) { uint64_t eventStartTimeNs = bucketStartTimeNs + 1; uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL; - OringDurationTracker tracker(wizard, 1, true, bucketStartTimeNs, bucketSizeNs, buckets); + OringDurationTracker tracker("event", wizard, 1, true, bucketStartTimeNs, bucketSizeNs, {}, + buckets); tracker.noteStart("2:maps", true, eventStartTimeNs, key1); tracker.noteStart("2:maps", true, eventStartTimeNs + 2, key1); @@ -141,7 +204,105 @@ TEST(OringDurationTrackerTest, TestDurationConditionChangeNested) { tracker.flushIfNeeded(bucketStartTimeNs + bucketSizeNs + 1); EXPECT_EQ(1u, buckets.size()); - EXPECT_EQ(15, buckets[0].mDuration); + EXPECT_EQ(15ULL, buckets[0].mDuration); +} + +TEST(OringDurationTrackerTest, TestPredictAnomalyTimestamp) { + Alert alert; + alert.set_name("alert"); + alert.set_metric_name("1"); + alert.set_trigger_if_sum_gt(40 * NS_PER_SEC); + alert.set_number_of_buckets(2); + alert.set_refractory_period_secs(1); + + vector buckets; + sp wizard = new NaggyMock(); + ConditionKey key1; + key1["APP_BACKGROUND"] = "1:maps|"; + uint64_t bucketStartTimeNs = 10 * NS_PER_SEC; + uint64_t eventStartTimeNs = bucketStartTimeNs + NS_PER_SEC + 1; + uint64_t bucketSizeNs = 30 * NS_PER_SEC; + + sp anomalyTracker = new AnomalyTracker(alert, bucketSizeNs); + OringDurationTracker tracker("event", wizard, 1, true, bucketStartTimeNs, bucketSizeNs, + {anomalyTracker}, buckets); + + // Nothing in the past bucket. + tracker.noteStart("", true, eventStartTimeNs, key1); + EXPECT_EQ((long long)(alert.trigger_if_sum_gt() + eventStartTimeNs), + tracker.predictAnomalyTimestampNs(*anomalyTracker, eventStartTimeNs)); + + tracker.noteStop("", eventStartTimeNs + 3, false); + EXPECT_EQ(0u, buckets.size()); + + uint64_t event1StartTimeNs = eventStartTimeNs + 10; + tracker.noteStart("1", true, event1StartTimeNs, key1); + // No past buckets. The anomaly will happen in bucket #0. + EXPECT_EQ((long long)(event1StartTimeNs + alert.trigger_if_sum_gt() - 3), + tracker.predictAnomalyTimestampNs(*anomalyTracker, event1StartTimeNs)); + + uint64_t event1StopTimeNs = eventStartTimeNs + bucketSizeNs + 10; + tracker.noteStop("1", event1StopTimeNs, false); + EXPECT_EQ(1u, buckets.size()); + EXPECT_EQ(3ULL + bucketStartTimeNs + bucketSizeNs - eventStartTimeNs - 10, + buckets[0].mDuration); + + const int64_t bucket0Duration = 3ULL + bucketStartTimeNs + bucketSizeNs - eventStartTimeNs - 10; + const int64_t bucket1Duration = eventStartTimeNs + 10 - bucketStartTimeNs; + + // One past buckets. The anomaly will happen in bucket #1. + uint64_t event2StartTimeNs = eventStartTimeNs + bucketSizeNs + 15; + tracker.noteStart("1", true, event2StartTimeNs, key1); + EXPECT_EQ((long long)(event2StartTimeNs + alert.trigger_if_sum_gt() - bucket0Duration - + bucket1Duration), + tracker.predictAnomalyTimestampNs(*anomalyTracker, event2StartTimeNs)); + tracker.noteStop("1", event2StartTimeNs + 1, false); + + // Only one past buckets is applicable. Bucket +0 should be trashed. The anomaly will happen in + // bucket #2. + uint64_t event3StartTimeNs = bucketStartTimeNs + 2 * bucketSizeNs - 9 * NS_PER_SEC; + tracker.noteStart("1", true, event3StartTimeNs, key1); + EXPECT_EQ((long long)(event3StartTimeNs + alert.trigger_if_sum_gt() - bucket1Duration - 1LL), + tracker.predictAnomalyTimestampNs(*anomalyTracker, event3StartTimeNs)); +} + +TEST(OringDurationTrackerTest, TestAnomalyDetection) { + Alert alert; + alert.set_name("alert"); + alert.set_metric_name("1"); + alert.set_trigger_if_sum_gt(40 * NS_PER_SEC); + alert.set_number_of_buckets(2); + alert.set_refractory_period_secs(1); + + vector buckets; + sp wizard = new NaggyMock(); + ConditionKey key1; + key1["APP_BACKGROUND"] = "1:maps|"; + uint64_t bucketStartTimeNs = 10 * NS_PER_SEC; + uint64_t eventStartTimeNs = bucketStartTimeNs + NS_PER_SEC + 1; + uint64_t bucketSizeNs = 30 * NS_PER_SEC; + + sp anomalyTracker = new AnomalyTracker(alert, bucketSizeNs); + OringDurationTracker tracker("event", wizard, 1, true /*nesting*/, bucketStartTimeNs, + bucketSizeNs, {anomalyTracker}, buckets); + + tracker.noteStart("", true, eventStartTimeNs, key1); + tracker.noteStop("", eventStartTimeNs + 10, false); + EXPECT_EQ(anomalyTracker->mLastAlarmTimestampNs, -1); + EXPECT_TRUE(tracker.mStarted.empty()); + EXPECT_EQ(-1LL, tracker.mLastStartTime); + EXPECT_EQ(10LL, tracker.mDuration); + + EXPECT_EQ(0u, tracker.mStarted.size()); + + tracker.noteStart("", true, eventStartTimeNs + 20, key1); + EXPECT_EQ(1u, anomalyTracker->mAlarms.size()); + EXPECT_EQ((long long)(51ULL * NS_PER_SEC), + (long long)(anomalyTracker->mAlarms.begin()->second->timestampSec * NS_PER_SEC)); + tracker.noteStop("", eventStartTimeNs + 2 * bucketSizeNs + 25, false); + EXPECT_EQ(anomalyTracker->getSumOverPastBuckets("event"), (long long)(bucketSizeNs)); + EXPECT_EQ((long long)(eventStartTimeNs + 2 * bucketSizeNs + 25), + anomalyTracker->mLastAlarmTimestampNs); } } // namespace statsd diff --git a/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp b/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp index cd647cb9c238a..1ed3636f4ec4f 100644 --- a/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp +++ b/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp @@ -277,7 +277,7 @@ TEST(ValueMetricProducerTest, TestPushedEventsWithoutCondition) { EXPECT_EQ(20, curInterval.raw.back().first); EXPECT_EQ(0UL, valueProducer.mNextSlicedBucket.size()); - valueProducer.flush_if_needed(bucket3StartTimeNs); + valueProducer.flushIfNeeded(bucket3StartTimeNs); EXPECT_EQ(1UL, valueProducer.mPastBuckets.size()); EXPECT_EQ(1UL, valueProducer.mPastBuckets.begin()->second.size()); EXPECT_EQ(30, valueProducer.mPastBuckets.begin()->second.back().mValue);