GenerateLinksLogger.java revision 5a03094ebc91df1c64a2232be648ac3ed26657ce
1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.view.textclassifier;
18
19import android.annotation.Nullable;
20import android.metrics.LogMaker;
21import android.util.ArrayMap;
22
23import com.android.internal.annotations.VisibleForTesting;
24import com.android.internal.logging.MetricsLogger;
25import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
26import com.android.internal.util.Preconditions;
27
28import java.util.Locale;
29import java.util.Map;
30import java.util.Objects;
31import java.util.Random;
32import java.util.UUID;
33
34/**
35 * A helper for logging calls to generateLinks.
36 * @hide
37 */
38@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
39public final class GenerateLinksLogger {
40
41    private static final String LOG_TAG = "GenerateLinksLogger";
42    private static final boolean DEBUG_LOG_ENABLED = false;
43    private static final String ZERO = "0";
44
45    private final MetricsLogger mMetricsLogger;
46    private final Random mRng;
47    private final int mSampleRate;
48
49    /**
50     * @param sampleRate the rate at which log events are written. (e.g. 100 means there is a 0.01
51     *                   chance that a call to logGenerateLinks results in an event being written).
52     *                   To write all events, pass 1.
53     */
54    public GenerateLinksLogger(int sampleRate) {
55        mSampleRate = sampleRate;
56        mRng = new Random(System.nanoTime());
57        mMetricsLogger = new MetricsLogger();
58    }
59
60    @VisibleForTesting
61    public GenerateLinksLogger(int sampleRate, MetricsLogger metricsLogger) {
62        mSampleRate = sampleRate;
63        mRng = new Random(System.nanoTime());
64        mMetricsLogger = metricsLogger;
65    }
66
67    /** Logs statistics about a call to generateLinks. */
68    public void logGenerateLinks(CharSequence text, TextLinks links, String callingPackageName,
69            long latencyMs) {
70        Preconditions.checkNotNull(text);
71        Preconditions.checkNotNull(links);
72        Preconditions.checkNotNull(callingPackageName);
73        if (!shouldLog()) {
74            return;
75        }
76
77        // Always populate the total stats, and per-entity stats for each entity type detected.
78        final LinkifyStats totalStats = new LinkifyStats();
79        final Map<String, LinkifyStats> perEntityTypeStats = new ArrayMap<>();
80        for (TextLinks.TextLink link : links.getLinks()) {
81            if (link.getEntityCount() == 0) continue;
82            final String entityType = link.getEntity(0);
83            if (entityType == null
84                    || TextClassifier.TYPE_OTHER.equals(entityType)
85                    || TextClassifier.TYPE_UNKNOWN.equals(entityType)) {
86                continue;
87            }
88            totalStats.countLink(link);
89            perEntityTypeStats.computeIfAbsent(entityType, k -> new LinkifyStats()).countLink(link);
90        }
91
92        final String callId = UUID.randomUUID().toString();
93        writeStats(callId, callingPackageName, null, totalStats, text, latencyMs);
94        for (Map.Entry<String, LinkifyStats> entry : perEntityTypeStats.entrySet()) {
95            writeStats(callId, callingPackageName, entry.getKey(), entry.getValue(), text,
96                       latencyMs);
97        }
98    }
99
100    /**
101     * Returns whether this particular event should be logged.
102     *
103     * Sampling is used to reduce the amount of logging data generated.
104     **/
105    private boolean shouldLog() {
106        if (mSampleRate <= 1) {
107            return true;
108        } else {
109            return mRng.nextInt(mSampleRate) == 0;
110        }
111    }
112
113    /** Writes a log event for the given stats. */
114    private void writeStats(String callId, String callingPackageName, @Nullable String entityType,
115                            LinkifyStats stats, CharSequence text, long latencyMs) {
116        final LogMaker log = new LogMaker(MetricsEvent.TEXT_CLASSIFIER_GENERATE_LINKS)
117                .setPackageName(callingPackageName)
118                .addTaggedData(MetricsEvent.FIELD_LINKIFY_CALL_ID, callId)
119                .addTaggedData(MetricsEvent.FIELD_LINKIFY_NUM_LINKS, stats.mNumLinks)
120                .addTaggedData(MetricsEvent.FIELD_LINKIFY_LINK_LENGTH, stats.mNumLinksTextLength)
121                .addTaggedData(MetricsEvent.FIELD_LINKIFY_TEXT_LENGTH, text.length())
122                .addTaggedData(MetricsEvent.FIELD_LINKIFY_LATENCY, latencyMs);
123        if (entityType != null) {
124            log.addTaggedData(MetricsEvent.FIELD_LINKIFY_ENTITY_TYPE, entityType);
125        }
126        mMetricsLogger.write(log);
127        debugLog(log);
128    }
129
130    private static void debugLog(LogMaker log) {
131        if (!DEBUG_LOG_ENABLED) return;
132
133        final String callId = Objects.toString(
134                log.getTaggedData(MetricsEvent.FIELD_LINKIFY_CALL_ID), "");
135        final String entityType = Objects.toString(
136                log.getTaggedData(MetricsEvent.FIELD_LINKIFY_ENTITY_TYPE), "ANY_ENTITY");
137        final int numLinks = Integer.parseInt(
138                Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_NUM_LINKS), ZERO));
139        final int linkLength = Integer.parseInt(
140                Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_LINK_LENGTH), ZERO));
141        final int textLength = Integer.parseInt(
142                Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_TEXT_LENGTH), ZERO));
143        final int latencyMs = Integer.parseInt(
144                Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_LATENCY), ZERO));
145
146        Log.d(LOG_TAG,
147                String.format(Locale.US, "%s:%s %d links (%d/%d chars) %dms %s", callId, entityType,
148                        numLinks, linkLength, textLength, latencyMs, log.getPackageName()));
149    }
150
151    /** Helper class for storing per-entity type statistics. */
152    private static final class LinkifyStats {
153        int mNumLinks;
154        int mNumLinksTextLength;
155
156        void countLink(TextLinks.TextLink link) {
157            mNumLinks += 1;
158            mNumLinksTextLength += link.getEnd() - link.getStart();
159        }
160    }
161}
162