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 android.car.telemetry; 18 19 import android.annotation.CallbackExecutor; 20 import android.annotation.IntDef; 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.annotation.RequiresPermission; 24 import android.annotation.SystemApi; 25 import android.annotation.TestApi; 26 import android.car.Car; 27 import android.car.CarManagerBase; 28 import android.car.annotation.AddedInOrBefore; 29 import android.car.annotation.RequiredFeature; 30 import android.car.builtin.util.Slogf; 31 import android.os.Bundle; 32 import android.os.IBinder; 33 import android.os.ParcelFileDescriptor; 34 import android.os.PersistableBundle; 35 import android.os.RemoteException; 36 import android.os.ResultReceiver; 37 38 import libcore.io.IoUtils; 39 40 import java.io.ByteArrayInputStream; 41 import java.io.DataInputStream; 42 import java.io.EOFException; 43 import java.io.IOException; 44 import java.lang.annotation.Retention; 45 import java.lang.annotation.RetentionPolicy; 46 import java.lang.ref.WeakReference; 47 import java.util.ArrayList; 48 import java.util.List; 49 import java.util.Objects; 50 import java.util.concurrent.Executor; 51 import java.util.concurrent.atomic.AtomicReference; 52 53 /** 54 * Provides an application interface for interacting with the Car Telemetry Service. 55 * 56 * @hide 57 */ 58 @RequiredFeature(Car.CAR_TELEMETRY_SERVICE) 59 @SystemApi 60 @TestApi 61 public final class CarTelemetryManager extends CarManagerBase { 62 63 private static final boolean DEBUG = false; 64 private static final String TAG = CarTelemetryManager.class.getSimpleName(); 65 private static final int METRICS_CONFIG_MAX_SIZE_BYTES = 10 * 1024; // 10 kb 66 67 private final ICarTelemetryService mService; 68 private final AtomicReference<Executor> mReportReadyListenerExecutor; 69 private final AtomicReference<ReportReadyListener> mReportReadyListener; 70 71 /** Status to indicate that MetricsConfig was added successfully. */ 72 @AddedInOrBefore(majorVersion = 33) 73 public static final int STATUS_ADD_METRICS_CONFIG_SUCCEEDED = 0; 74 75 /** 76 * Status to indicate that add MetricsConfig failed because the same MetricsConfig of the same 77 * name and version already exists. 78 */ 79 @AddedInOrBefore(majorVersion = 33) 80 public static final int STATUS_ADD_METRICS_CONFIG_ALREADY_EXISTS = 1; 81 82 /** 83 * Status to indicate that add MetricsConfig failed because a newer version of the MetricsConfig 84 * exists. 85 */ 86 @AddedInOrBefore(majorVersion = 33) 87 public static final int STATUS_ADD_METRICS_CONFIG_VERSION_TOO_OLD = 2; 88 89 /** 90 * Status to indicate that add MetricsConfig failed because CarTelemetryService is unable to 91 * parse the given byte array into a MetricsConfig. 92 */ 93 @AddedInOrBefore(majorVersion = 33) 94 public static final int STATUS_ADD_METRICS_CONFIG_PARSE_FAILED = 3; 95 96 /** 97 * Status to indicate that add MetricsConfig failed because of failure to verify the signature 98 * of the MetricsConfig. 99 */ 100 @AddedInOrBefore(majorVersion = 33) 101 public static final int STATUS_ADD_METRICS_CONFIG_SIGNATURE_VERIFICATION_FAILED = 4; 102 103 /** Status to indicate that add MetricsConfig failed because of a general error in cars. */ 104 @AddedInOrBefore(majorVersion = 33) 105 public static final int STATUS_ADD_METRICS_CONFIG_UNKNOWN = 5; 106 107 /** @hide */ 108 @IntDef( 109 prefix = {"STATUS_ADD_METRICS_CONFIG_"}, 110 value = { 111 STATUS_ADD_METRICS_CONFIG_SUCCEEDED, 112 STATUS_ADD_METRICS_CONFIG_ALREADY_EXISTS, 113 STATUS_ADD_METRICS_CONFIG_VERSION_TOO_OLD, 114 STATUS_ADD_METRICS_CONFIG_PARSE_FAILED, 115 STATUS_ADD_METRICS_CONFIG_SIGNATURE_VERIFICATION_FAILED, 116 STATUS_ADD_METRICS_CONFIG_UNKNOWN 117 }) 118 @Retention(RetentionPolicy.SOURCE) 119 public @interface MetricsConfigStatus {} 120 121 /** Status to indicate that MetricsConfig produced a report. */ 122 @AddedInOrBefore(majorVersion = 33) 123 public static final int STATUS_GET_METRICS_CONFIG_FINISHED = 0; 124 125 /** 126 * Status to indicate a MetricsConfig exists but has produced neither interim/final report nor 127 * runtime execution errors. 128 */ 129 @AddedInOrBefore(majorVersion = 33) 130 public static final int STATUS_GET_METRICS_CONFIG_PENDING = 1; 131 132 /** Status to indicate a MetricsConfig exists and produced interim results. */ 133 @AddedInOrBefore(majorVersion = 33) 134 public static final int STATUS_GET_METRICS_CONFIG_INTERIM_RESULTS = 2; 135 136 /** Status to indicate the MetricsConfig produced a runtime execution error. */ 137 @AddedInOrBefore(majorVersion = 33) 138 public static final int STATUS_GET_METRICS_CONFIG_RUNTIME_ERROR = 3; 139 140 /** Status to indicate a MetricsConfig does not exist and hence no report can be found. */ 141 @AddedInOrBefore(majorVersion = 33) 142 public static final int STATUS_GET_METRICS_CONFIG_DOES_NOT_EXIST = 4; 143 144 /** @hide */ 145 @IntDef( 146 prefix = {"STATUS_GET_METRICS_CONFIG_"}, 147 value = { 148 STATUS_GET_METRICS_CONFIG_FINISHED, 149 STATUS_GET_METRICS_CONFIG_PENDING, 150 STATUS_GET_METRICS_CONFIG_INTERIM_RESULTS, 151 STATUS_GET_METRICS_CONFIG_RUNTIME_ERROR, 152 STATUS_GET_METRICS_CONFIG_DOES_NOT_EXIST 153 }) 154 @Retention(RetentionPolicy.SOURCE) 155 public @interface MetricsReportStatus {} 156 157 /** 158 * Application must pass a {@link AddMetricsConfigCallback} to use {@link 159 * #addMetricsConfig(String, byte[], Executor, AddMetricsConfigCallback)} 160 * 161 * @hide 162 */ 163 @SystemApi 164 @TestApi 165 public interface AddMetricsConfigCallback { 166 /** 167 * Sends the {@link #addMetricsConfig(String, byte[], Executor, AddMetricsConfigCallback)} 168 * status to the client. 169 * 170 * @param metricsConfigName name of the MetricsConfig that the status is associated with. 171 * @param statusCode See {@link MetricsConfigStatus}. 172 */ 173 @AddedInOrBefore(majorVersion = 33) onAddMetricsConfigStatus( @onNull String metricsConfigName, @MetricsConfigStatus int statusCode)174 void onAddMetricsConfigStatus( 175 @NonNull String metricsConfigName, @MetricsConfigStatus int statusCode); 176 } 177 178 /** 179 * Application must pass a {@link MetricsReportCallback} object to receive finished reports from 180 * {@link #getFinishedReport(String, Executor, MetricsReportCallback)} and {@link 181 * #getAllFinishedReports(Executor, MetricsReportCallback)}. 182 * 183 * @hide 184 */ 185 @SystemApi 186 @TestApi 187 public interface MetricsReportCallback { 188 /** 189 * Provides the metrics report associated with metricsConfigName. If there is a metrics 190 * report, it provides the metrics report. If the metrics report calculation failed due to a 191 * runtime error during the execution of reporting script, it provides the runtime error in 192 * the error parameter. The status parameter provides more information on the state of the 193 * metrics report. 194 * 195 * TODO(b/184964661): Publish the documentation for the format of the finished reports. 196 * 197 * @param metricsConfigName name of the MetricsConfig that the report is associated with. 198 * @param report the car telemetry report. Null if there is no report. 199 * @param telemetryError the serialized telemetry metrics configuration runtime execution 200 * error. 201 * @param status of the metrics report. See {@link MetricsReportStatus}. 202 */ 203 @AddedInOrBefore(majorVersion = 33) onResult( @onNull String metricsConfigName, @Nullable PersistableBundle report, @Nullable byte[] telemetryError, @MetricsReportStatus int status)204 void onResult( 205 @NonNull String metricsConfigName, 206 @Nullable PersistableBundle report, 207 @Nullable byte[] telemetryError, 208 @MetricsReportStatus int status); 209 } 210 211 /** 212 * Application can optionally use {@link #setReportReadyListener(Executor, ReportReadyListener)} 213 * to receive report ready notifications. Upon receiving the notification, client can use 214 * {@link #getFinishedReport(String, Executor, MetricsReportCallback)} on the received 215 * metricsConfigName. 216 * 217 * @hide 218 */ 219 @SystemApi 220 @TestApi 221 public interface ReportReadyListener { 222 /** 223 * Sends the report ready notification to the client. 224 * 225 * @param metricsConfigName name of the MetricsConfig whose report is ready. 226 */ 227 @AddedInOrBefore(majorVersion = 33) onReady(@onNull String metricsConfigName)228 void onReady(@NonNull String metricsConfigName); 229 } 230 231 /** 232 * Gets an instance of CarTelemetryManager. 233 * 234 * <p>CarTelemetryManager manages {@link com.android.car.telemetry.CarTelemetryService} and 235 * provides APIs so the client can use the car telemetry service. 236 * 237 * <p>There is only one client to this manager, which is OEM's cloud application. It uses the 238 * APIs to send config to and receive data from CarTelemetryService. 239 * 240 * @hide 241 */ CarTelemetryManager(Car car, IBinder service)242 public CarTelemetryManager(Car car, IBinder service) { 243 super(car); 244 mService = ICarTelemetryService.Stub.asInterface(service); 245 mReportReadyListenerExecutor = new AtomicReference<>(null); 246 mReportReadyListener = new AtomicReference<>(null); 247 if (DEBUG) { 248 Slogf.d(TAG, "starting car telemetry manager"); 249 } 250 } 251 252 /** @hide */ 253 @Override 254 @AddedInOrBefore(majorVersion = 33) onCarDisconnected()255 public void onCarDisconnected() {} 256 257 /** 258 * Adds a MetricsConfig to CarTelemetryService. The size of the MetricsConfig cannot exceed a 259 * {@link #METRICS_CONFIG_MAX_SIZE_BYTES}, otherwise an exception is thrown. 260 * 261 * <p>The MetricsConfig will be uniquely identified by its name and version. If a MetricsConfig 262 * of the same name already exists in {@link com.android.car.telemetry.CarTelemetryService}, the 263 * config version will be compared. If the version is strictly higher, the existing 264 * MetricsConfig will be replaced by the new one. All legacy data will be cleared if replaced. 265 * 266 * <p>Client should use {@link #getFinishedReport(String, Executor, MetricsReportCallback)} to 267 * get the report before replacing a MetricsConfig. 268 * 269 * <p>The status of this API is sent back asynchronously via {@link AddMetricsConfigCallback}. 270 * 271 * @param metricsConfigName name of the MetricsConfig, must match {@link 272 * TelemetryProto.MetricsConfig#getName()}. 273 * @param metricsConfig the serialized bytes of a MetricsConfig object. 274 * @param executor The {@link Executor} on which the callback will be invoked. 275 * @param callback A callback for receiving addMetricsConfig status codes. 276 * @throws IllegalArgumentException if the MetricsConfig size exceeds limit. 277 * @hide 278 */ 279 @SystemApi 280 @TestApi 281 @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE) 282 @AddedInOrBefore(majorVersion = 33) addMetricsConfig( @onNull String metricsConfigName, @NonNull byte[] metricsConfig, @CallbackExecutor @NonNull Executor executor, @NonNull AddMetricsConfigCallback callback)283 public void addMetricsConfig( 284 @NonNull String metricsConfigName, 285 @NonNull byte[] metricsConfig, 286 @CallbackExecutor @NonNull Executor executor, 287 @NonNull AddMetricsConfigCallback callback) { 288 if (metricsConfig.length > METRICS_CONFIG_MAX_SIZE_BYTES) { 289 throw new IllegalArgumentException("MetricsConfig size exceeds limit."); 290 } 291 try { 292 mService.addMetricsConfig(metricsConfigName, metricsConfig, new ResultReceiver(null) { 293 @Override 294 protected void onReceiveResult(int resultCode, Bundle resultData) { 295 executor.execute(() -> 296 callback.onAddMetricsConfigStatus(metricsConfigName, resultCode)); 297 } 298 }); 299 } catch (RemoteException e) { 300 handleRemoteExceptionFromCarService(e); 301 } 302 } 303 304 /** 305 * Removes a MetricsConfig from {@link com.android.car.telemetry.CarTelemetryService}. This will 306 * also remove outputs produced by the MetricsConfig. If the MetricsConfig does not exist, 307 * nothing will be removed. 308 * 309 * @param metricsConfigName that identify the MetricsConfig. 310 * @hide 311 */ 312 @SystemApi 313 @TestApi 314 @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE) 315 @AddedInOrBefore(majorVersion = 33) removeMetricsConfig(@onNull String metricsConfigName)316 public void removeMetricsConfig(@NonNull String metricsConfigName) { 317 try { 318 mService.removeMetricsConfig(metricsConfigName); 319 } catch (RemoteException e) { 320 handleRemoteExceptionFromCarService(e); 321 } 322 } 323 324 /** 325 * Removes all MetricsConfigs from {@link com.android.car.telemetry.CarTelemetryService}. This 326 * will also remove all MetricsConfig outputs. 327 * 328 * @hide 329 */ 330 @SystemApi 331 @TestApi 332 @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE) 333 @AddedInOrBefore(majorVersion = 33) removeAllMetricsConfigs()334 public void removeAllMetricsConfigs() { 335 try { 336 mService.removeAllMetricsConfigs(); 337 } catch (RemoteException e) { 338 handleRemoteExceptionFromCarService(e); 339 } 340 } 341 342 /** 343 * Gets script execution reports of a MetricsConfig as from the {@link 344 * com.android.car.telemetry.CarTelemetryService}. This API is asynchronous and the report is 345 * sent back asynchronously via the {@link MetricsReportCallback}. This call is destructive. The 346 * returned report will be deleted from CarTelemetryService. 347 * 348 * @param metricsConfigName to identify the MetricsConfig. 349 * @param executor The {@link Executor} on which the callback will be invoked. 350 * @param callback A callback for receiving finished reports. 351 * @hide 352 */ 353 @SystemApi 354 @TestApi 355 @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE) 356 @AddedInOrBefore(majorVersion = 33) getFinishedReport( @onNull String metricsConfigName, @CallbackExecutor @NonNull Executor executor, @NonNull MetricsReportCallback callback)357 public void getFinishedReport( 358 @NonNull String metricsConfigName, 359 @CallbackExecutor @NonNull Executor executor, 360 @NonNull MetricsReportCallback callback) { 361 try { 362 mService.getFinishedReport( 363 metricsConfigName, new CarTelemetryReportListenerImpl(executor, callback)); 364 } catch (RemoteException e) { 365 handleRemoteExceptionFromCarService(e); 366 } 367 } 368 369 /** 370 * Gets all script execution reports from {@link com.android.car.telemetry.CarTelemetryService} 371 * asynchronously via the {@link MetricsReportCallback}. The callback will be invoked multiple 372 * times if there are multiple reports. This call is destructive. The returned reports will be 373 * deleted from CarTelemetryService. 374 * 375 * @param executor The {@link Executor} on which the callback will be invoked. 376 * @param callback A callback for receiving finished reports. 377 * @hide 378 */ 379 @SystemApi 380 @TestApi 381 @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE) 382 @AddedInOrBefore(majorVersion = 33) getAllFinishedReports( @allbackExecutor @onNull Executor executor, @NonNull MetricsReportCallback callback)383 public void getAllFinishedReports( 384 @CallbackExecutor @NonNull Executor executor, @NonNull MetricsReportCallback callback) { 385 try { 386 mService.getAllFinishedReports(new CarTelemetryReportListenerImpl(executor, callback)); 387 } catch (RemoteException e) { 388 handleRemoteExceptionFromCarService(e); 389 } 390 } 391 392 /** 393 * Registers a listener to receive report ready notifications. This is an optional feature that 394 * helps clients decide when is a good time to call {@link 395 * #getFinishedReport(String, Executor, MetricsReportCallback)}. 396 * 397 * <p>When a listener is set, it will receive notifications for reports or errors that are 398 * already produced before the listener is registered. 399 * 400 * <p>Clients who do not register a listener should use {@link 401 * #getFinishedReport(String, Executor, MetricsReportCallback)} periodically to check for 402 * report. 403 * 404 * @param executor The {@link Executor} on which the callback will be invoked. 405 * @param listener The listener to receive report ready notifications. 406 * @throws IllegalStateException if the listener is already set. 407 * @hide 408 */ 409 @SystemApi 410 @TestApi 411 @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE) 412 @AddedInOrBefore(majorVersion = 33) setReportReadyListener( @allbackExecutor @onNull Executor executor, @NonNull ReportReadyListener listener)413 public void setReportReadyListener( 414 @CallbackExecutor @NonNull Executor executor, @NonNull ReportReadyListener listener) { 415 if (mReportReadyListener.get() != null) { 416 throw new IllegalStateException("ReportReadyListener is already set."); 417 } 418 mReportReadyListenerExecutor.set(executor); 419 mReportReadyListener.set(listener); 420 try { 421 mService.setReportReadyListener(new CarTelemetryReportReadyListenerImpl(this)); 422 } catch (RemoteException e) { 423 handleRemoteExceptionFromCarService(e); 424 } 425 } 426 427 /** 428 * Clears the listener for receiving telemetry report ready notifications. 429 * 430 * @hide 431 */ 432 @SystemApi 433 @TestApi 434 @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE) 435 @AddedInOrBefore(majorVersion = 33) clearReportReadyListener()436 public void clearReportReadyListener() { 437 mReportReadyListenerExecutor.set(null); 438 mReportReadyListener.set(null); 439 try { 440 mService.clearReportReadyListener(); 441 } catch (RemoteException e) { 442 handleRemoteExceptionFromCarService(e); 443 } 444 } 445 446 /** Listens for report ready notifications. 447 * Atomic variables (mReportReadyListenerExecutor and mReportReadyListener) 448 * can be accessed from different threads simultaneously. 449 * Both of these variables can be set to null by {@link #clearReportReadyListener()} 450 * and simultaneously {@link #onReady(String)} may try to access the null value. 451 * So, to avoid possible NullPointerException due to this race condition, 452 * these atomic variables are needed to be retrieved in local variables 453 * and verified those are not null before accessing. */ 454 private static final class CarTelemetryReportReadyListenerImpl 455 extends ICarTelemetryReportReadyListener.Stub { 456 private final WeakReference<CarTelemetryManager> mManager; 457 CarTelemetryReportReadyListenerImpl(CarTelemetryManager manager)458 private CarTelemetryReportReadyListenerImpl(CarTelemetryManager manager) { 459 mManager = new WeakReference<>(manager); 460 } 461 462 @Override onReady(@onNull String metricsConfigName)463 public void onReady(@NonNull String metricsConfigName) { 464 CarTelemetryManager manager = mManager.get(); 465 if (manager == null) { 466 return; 467 } 468 Executor executor = manager.mReportReadyListenerExecutor.get(); 469 if (executor == null) { 470 return; 471 } 472 ReportReadyListener reportReadyListener = manager.mReportReadyListener.get(); 473 if (reportReadyListener == null) { 474 return; 475 } 476 executor.execute( 477 () -> reportReadyListener.onReady(metricsConfigName)); 478 } 479 } 480 481 /** 482 * Receives responses to {@link #getFinishedReport(String, Executor, MetricsReportCallback)} 483 * requests. 484 */ 485 private static final class CarTelemetryReportListenerImpl 486 extends ICarTelemetryReportListener.Stub { 487 488 private final Executor mExecutor; 489 private final MetricsReportCallback mMetricsReportCallback; 490 CarTelemetryReportListenerImpl(Executor executor, MetricsReportCallback callback)491 private CarTelemetryReportListenerImpl(Executor executor, MetricsReportCallback callback) { 492 Objects.requireNonNull(executor); 493 Objects.requireNonNull(callback); 494 mExecutor = executor; 495 mMetricsReportCallback = callback; 496 } 497 498 @Override onResult( @onNull String metricsConfigName, @Nullable ParcelFileDescriptor reportFileDescriptor, @Nullable byte[] telemetryError, @MetricsReportStatus int status)499 public void onResult( 500 @NonNull String metricsConfigName, 501 @Nullable ParcelFileDescriptor reportFileDescriptor, 502 @Nullable byte[] telemetryError, 503 @MetricsReportStatus int status) { 504 // return early if no need to stream reports 505 if (reportFileDescriptor == null) { 506 mExecutor.execute(() -> mMetricsReportCallback.onResult( 507 metricsConfigName, null, telemetryError, status)); 508 return; 509 } 510 // getting to this line means the reportFileDescriptor is non-null 511 ParcelFileDescriptor dup = null; 512 try { 513 dup = reportFileDescriptor.dup(); 514 } catch (IOException e) { 515 Slogf.w(TAG, "Could not dup ParcelFileDescriptor", e); 516 return; 517 } finally { 518 IoUtils.closeQuietly(reportFileDescriptor); 519 } 520 final ParcelFileDescriptor readFd = dup; 521 mExecutor.execute(() -> { 522 // read PersistableBundles from the pipe, this method will also close the fd 523 List<PersistableBundle> reports = parseReports(readFd); 524 // if a readFd is non-null, CarTelemetryService will write at least 1 report 525 // to the pipe, so something must have gone wrong to get 0 report 526 if (reports.size() == 0) { 527 mMetricsReportCallback.onResult(metricsConfigName, null, null, 528 STATUS_GET_METRICS_CONFIG_RUNTIME_ERROR); 529 return; 530 } 531 for (PersistableBundle report : reports) { 532 mMetricsReportCallback 533 .onResult(metricsConfigName, report, telemetryError, status); 534 } 535 }); 536 } 537 538 /** Helper method to parse reports (PersistableBundles) from the file descriptor. */ parseReports(ParcelFileDescriptor reportFileDescriptor)539 private List<PersistableBundle> parseReports(ParcelFileDescriptor reportFileDescriptor) { 540 List<PersistableBundle> reports = new ArrayList<>(); 541 try (DataInputStream dataInputStream = new DataInputStream( 542 new ParcelFileDescriptor.AutoCloseInputStream(reportFileDescriptor))) { 543 while (true) { 544 // read integer which tells us how many bytes to read for the PersistableBundle 545 int size = dataInputStream.readInt(); 546 byte[] bundleBytes = dataInputStream.readNBytes(size); 547 if (bundleBytes.length != size) { 548 Slogf.e(TAG, "Expected to read " + size 549 + " bytes from the pipe, but only read " 550 + bundleBytes.length + " bytes"); 551 break; 552 } 553 PersistableBundle report = PersistableBundle.readFromStream( 554 new ByteArrayInputStream(bundleBytes)); 555 reports.add(report); 556 } 557 } catch (EOFException e) { 558 // a graceful exit from the while true loop, thrown by DataInputStream#readInt(), 559 // every successful parse should naturally reach this line 560 if (DEBUG) { 561 Slogf.d(TAG, "parseReports reached end of file"); 562 } 563 } catch (IOException e) { 564 Slogf.e(TAG, "Failed to read metrics reports from pipe", e); 565 } 566 return reports; 567 } 568 } 569 } 570