1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.bedstead.metricsrecorder;
18 
19 import android.util.Log;
20 
21 import com.android.bedstead.nene.exceptions.NeneException;
22 import com.android.queryable.Queryable;
23 import com.android.queryable.queries.BooleanQuery;
24 import com.android.queryable.queries.BooleanQueryHelper;
25 import com.android.queryable.queries.IntegerQuery;
26 import com.android.queryable.queries.IntegerQueryHelper;
27 import com.android.queryable.queries.ListQueryHelper;
28 import com.android.queryable.queries.StringQuery;
29 import com.android.queryable.queries.StringQueryHelper;
30 
31 import java.time.Duration;
32 import java.time.Instant;
33 import java.util.HashSet;
34 import java.util.Set;
35 
36 /**
37  * {@link Queryable} for querying logged metrics.
38  */
39 public class MetricQueryBuilder implements Queryable {
40 
41     private static final String LOG_TAG = "MetricQueryBuilder";
42 
43     private final EnterpriseMetricsRecorder mRecorder;
44     private boolean hasStartedFetchingResults = false;
45     private int mSkippedNextResults = 0;
46     private int mSkippedPollResults = 0;
47     private Set<EnterpriseMetricInfo> mNonMatchingMetrics = new HashSet<>();
48 
49     private final IntegerQueryHelper<MetricQueryBuilder> mTypeQuery =
50             new IntegerQueryHelper<>(this);
51     private final StringQueryHelper<MetricQueryBuilder> mAdminPackageNameQuery =
52             new StringQueryHelper<>(this);
53     private final BooleanQueryHelper<MetricQueryBuilder> mBooleanQuery =
54             new BooleanQueryHelper<>(this);
55     private final ListQueryHelper<MetricQueryBuilder, String> mStringsQuery =
56             new ListQueryHelper<>(this);
57     private final IntegerQueryHelper<MetricQueryBuilder> mIntegerQuery =
58             new IntegerQueryHelper<>(this);
59 
MetricQueryBuilder(EnterpriseMetricsRecorder recorder)60     MetricQueryBuilder(EnterpriseMetricsRecorder recorder) {
61         mRecorder = recorder;
62     }
63 
64     /** Query for {@link EnterpriseMetricInfo#type()}. */
whereType()65     public IntegerQuery<MetricQueryBuilder> whereType() {
66         if (hasStartedFetchingResults) {
67             throw new IllegalStateException("Cannot modify query after fetching results");
68         }
69         return mTypeQuery;
70     }
71 
72     /** Query for {@link EnterpriseMetricInfo#adminPackageName()}. */
whereAdminPackageName()73     public StringQuery<MetricQueryBuilder> whereAdminPackageName() {
74         if (hasStartedFetchingResults) {
75             throw new IllegalStateException("Cannot modify query after fetching results");
76         }
77         return mAdminPackageNameQuery;
78     }
79 
80     /** Query for {@link EnterpriseMetricInfo#Boolean()}. */
whereBoolean()81     public BooleanQuery<MetricQueryBuilder> whereBoolean() {
82         if (hasStartedFetchingResults) {
83             throw new IllegalStateException("Cannot modify query after fetching results");
84         }
85         return mBooleanQuery;
86     }
87 
88     /** Query for {@link EnterpriseMetricInfo#integer()}. */
whereInteger()89     public IntegerQuery<MetricQueryBuilder> whereInteger() {
90         if (hasStartedFetchingResults) {
91             throw new IllegalStateException("Cannot modify query after fetching results");
92         }
93         return mIntegerQuery;
94     }
95 
whereStrings()96     public ListQueryHelper<MetricQueryBuilder, String> whereStrings() {
97         if (hasStartedFetchingResults) {
98             throw new IllegalStateException("Cannot modify query after fetching results");
99         }
100         return mStringsQuery;
101     }
102 
get()103     public EnterpriseMetricInfo get() {
104         return get(/* skipResults= */ 0);
105     }
106 
get(int skipResults)107     private EnterpriseMetricInfo get(int skipResults) {
108         hasStartedFetchingResults = true;
109         for (EnterpriseMetricInfo m : mRecorder.fetchLatestData()) {
110             if (matches(m)) {
111                 skipResults -= 1;
112                 if (skipResults < 0) {
113                     return m;
114                 }
115             } else {
116                 Log.d(LOG_TAG, "Found non-matching metric " + m);
117                 mNonMatchingMetrics.add(m);
118             }
119         }
120 
121         return null;
122     }
123 
next()124     public EnterpriseMetricInfo next() {
125         hasStartedFetchingResults = true;
126 
127         EnterpriseMetricInfo nextResult = get(mSkippedNextResults);
128         if (nextResult != null) {
129             mSkippedNextResults++;
130         }
131 
132         return nextResult;
133     }
134 
poll()135     public EnterpriseMetricInfo poll() {
136         return poll(Duration.ofSeconds(30));
137     }
138 
poll(Duration timeout)139     public EnterpriseMetricInfo poll(Duration timeout) {
140         hasStartedFetchingResults = true;
141         Instant endTime = Instant.now().plus(timeout);
142 
143         while (Instant.now().isBefore(endTime)) {
144             EnterpriseMetricInfo nextResult = get(mSkippedPollResults);
145             if (nextResult != null) {
146                 mSkippedPollResults++;
147                 return nextResult;
148             }
149 
150             try {
151                 Thread.sleep(500);
152             } catch (InterruptedException e) {
153                 throw new NeneException("Interrupted while polling", e);
154             }
155         }
156 
157         return null;
158     }
159 
160     /**
161      * Get metrics which were received but didn't match the query.
162      */
nonMatchingMetrics()163     public Set<EnterpriseMetricInfo> nonMatchingMetrics() {
164         return mNonMatchingMetrics;
165     }
166 
167     @Override
isEmptyQuery()168     public boolean isEmptyQuery() {
169         return Queryable.isEmptyQuery(mAdminPackageNameQuery)
170                 && Queryable.isEmptyQuery(mTypeQuery)
171                 && Queryable.isEmptyQuery(mBooleanQuery)
172                 && Queryable.isEmptyQuery(mStringsQuery)
173                 && Queryable.isEmptyQuery(mIntegerQuery);
174     }
175 
matches(EnterpriseMetricInfo metric)176     private boolean matches(EnterpriseMetricInfo metric) {
177         return mAdminPackageNameQuery.matches(metric.adminPackageName())
178                 && mTypeQuery.matches(metric.type())
179                 && mBooleanQuery.matches(metric.Boolean())
180                 && mStringsQuery.matches(metric.strings())
181                 && mIntegerQuery.matches(metric.integer());
182     }
183 
184     @Override
describeQuery(String fieldName)185     public String describeQuery(String fieldName) {
186         return "{" + Queryable.joinQueryStrings(
187                 mAdminPackageNameQuery.describeQuery("adminPackageName"),
188                 mTypeQuery.describeQuery("type"),
189                 mBooleanQuery.describeQuery("boolean"),
190                 mStringsQuery.describeQuery("strings"),
191                 mIntegerQuery.describeQuery("integer")
192         ) + "}";
193     }
194 
195     @Override
toString()196     public String toString() {
197         return describeQuery("");
198     }
199 }
200