Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / appstats / MemcacheWriter.java
bloba8a0af4a00a3a4caedf9b54ee0d4e4dcdcb209ee
1 // Copyright 2009 Google Inc. All Rights Reserved.
3 package com.google.appengine.tools.appstats;
5 import com.google.appengine.api.memcache.Expiration;
6 import com.google.appengine.api.memcache.MemcacheService;
7 import com.google.appengine.api.memcache.MemcacheServiceException;
8 import com.google.appengine.tools.appstats.Recorder.Clock;
9 import com.google.appengine.tools.appstats.StatsProtos.AggregateRpcStatsProto;
10 import com.google.appengine.tools.appstats.StatsProtos.BilledOpProto;
11 import com.google.appengine.tools.appstats.StatsProtos.BilledOpProto.BilledOp;
12 import com.google.appengine.tools.appstats.StatsProtos.IndividualRpcStatsProto;
13 import com.google.appengine.tools.appstats.StatsProtos.RequestStatProto;
14 import com.google.appengine.tools.appstats.StatsProtos.RequestStatProto.Builder;
15 import com.google.apphosting.api.ApiProxy.Delegate;
16 import com.google.apphosting.api.ApiProxy.Environment;
17 import com.google.common.collect.Maps;
18 import com.google.protobuf.InvalidProtocolBufferException;
19 import com.google.protobuf.Descriptors.FieldDescriptor;
21 import java.util.ArrayList;
22 import java.util.HashMap;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.logging.Level;
26 import java.util.logging.Logger;
28 import javax.servlet.http.HttpServletRequest;
30 /**
31 * Persists stats information in memcache. Can also be used to access that data
32 * again. This class is thread-safe, assuming that the underlying MemcacheService
33 * is.
36 class MemcacheWriter implements Recorder.RecordWriter {
38 private static final int FIRST_FIELD_NUMBER_FOR_DETAILS = 100;
40 private static final String KEY_PREFIX = "__appstats__";
42 private static final String KEY_TEMPLATE = ":%06d";
44 private static final String PART_SUFFIX = ":part";
46 private static final String FULL_SUFFIX = ":full";
48 private static final int KEY_DISTANCE = 100;
50 private static final int KEY_MODULUS = 1000;
52 static final int MAX_SIZE = 1000000;
54 private static final int EXPIRATION_SECONDS = 36 * 3600;
56 public static final String STATS_NAMESPACE = "__appstats__";
58 private static String makeKeyPrefix(long timestamp) {
59 return String.format(
60 KEY_PREFIX + KEY_TEMPLATE,
61 ((timestamp / KEY_DISTANCE) % KEY_MODULUS) * KEY_DISTANCE);
64 protected final Logger log = Logger.getLogger(getClass().getName());
65 private final Recorder.Clock clock;
67 String keyInCache;
69 private final MemcacheService statsMemcache;
71 public MemcacheWriter(Clock clock, MemcacheService service) {
72 this.clock = clock;
73 this.keyInCache = getClass().getName() + ".CACHED_STATS";
74 this.statsMemcache = service;
75 if (service == null) {
76 throw new NullPointerException("Memcache service not found");
80 @Override
81 public final Long begin(
82 Delegate<?> wrappedDelegate, Environment environment, HttpServletRequest request) {
83 long beganAt = clock.currentTimeMillis();
85 RequestStatProto.Builder builder = RequestStatProto.newBuilder();
86 builder.setStartTimestampMilliseconds(beganAt);
87 builder.setHttpMethod(request.getMethod());
88 builder.setHttpPath(request.getRequestURI());
89 String queryUnescaped = request.getQueryString();
90 if (queryUnescaped != null && queryUnescaped.length() > 0) {
91 builder.setHttpQuery("?" + queryUnescaped);
94 builder.setIsAdmin(environment.isAdmin());
95 if (environment.getEmail() != null) {
96 builder.setUserEmail(environment.getEmail());
99 builder.setOverheadWalltimeMilliseconds(clock.currentTimeMillis() - beganAt);
100 environment.getAttributes().put(keyInCache, builder);
101 return beganAt;
104 @Override
105 public final boolean commit(
106 Delegate<?> wrappedDelegate, Environment environment, Integer responseCode) {
108 RequestStatProto.Builder builder =
109 (RequestStatProto.Builder) environment.getAttributes().get(keyInCache);
110 if (builder == null) {
111 return false;
114 builder.setDurationMilliseconds(
115 clock.currentTimeMillis() - builder.getStartTimestampMilliseconds());
117 if (responseCode != null) {
118 builder.setHttpStatus(responseCode);
119 } else {
120 builder.clearHttpStatus();
123 Map<String, AggregateRpcStatsProto.Builder> aggregates =
124 new HashMap<String, AggregateRpcStatsProto.Builder>();
125 for (IndividualRpcStatsProto stat : builder.getIndividualStatsList()) {
126 String key = stat.getServiceCallName();
127 if (!aggregates.containsKey(key)) {
128 AggregateRpcStatsProto.Builder aggregate = AggregateRpcStatsProto.newBuilder()
129 .setServiceCallName(stat.getServiceCallName())
130 .setTotalAmountOfCalls(0L);
131 if (stat.hasCallCostMicrodollars()) {
132 aggregate.setTotalCostOfCallsMicrodollars(0L);
134 aggregates.put(key, aggregate);
136 Map<BilledOp, BilledOpProto.Builder> billedOpMap = Maps.newHashMap();
137 AggregateRpcStatsProto.Builder aggregate = aggregates.get(key);
138 aggregate.setTotalAmountOfCalls(aggregate.getTotalAmountOfCalls() + 1);
139 aggregate.setTotalCostOfCallsMicrodollars(
140 aggregate.getTotalCostOfCallsMicrodollars() + stat.getCallCostMicrodollars());
141 for (BilledOpProto billedOp : stat.getBilledOpsList()) {
142 BilledOpProto.Builder totalBilledOp = billedOpMap.get(billedOp.getOp());
143 if (totalBilledOp == null) {
144 totalBilledOp = BilledOpProto.newBuilder().setOp(billedOp.getOp());
145 billedOpMap.put(billedOp.getOp(), totalBilledOp);
147 totalBilledOp.setNumOps(totalBilledOp.getNumOps() + billedOp.getNumOps());
149 for (BilledOpProto.Builder totalBilledOp : billedOpMap.values()) {
150 aggregate.addTotalBilledOps(totalBilledOp);
153 for (AggregateRpcStatsProto.Builder aggregate : aggregates.values()) {
154 builder.addRpcStats(aggregate);
157 environment.getAttributes().remove(keyInCache);
159 try {
160 persist(builder.build());
161 return true;
162 } catch (MemcacheServiceException e) {
163 log.log(Level.WARNING, "Error persisting request stats", e);
164 return false;
168 @Override
169 public final void write(
170 Delegate<?> wrappedDelegate,
171 Environment environment,
172 IndividualRpcStatsProto.Builder record,
173 long overheadWalltimeMillis,
174 boolean correctStartOffset) {
175 if (record == null) {
176 throw new NullPointerException("Record must not be null");
178 if (environment == null) {
179 throw new NullPointerException("Environment must not be null");
181 RequestStatProto.Builder builder =
182 (RequestStatProto.Builder) environment.getAttributes().get(keyInCache);
183 if (builder != null) {
184 if (correctStartOffset) {
185 record.setStartOffsetMilliseconds(Math.max(0, record.getStartOffsetMilliseconds()
186 - builder.getStartTimestampMilliseconds()));
188 synchronized (builder) {
189 builder.addIndividualStats(record);
190 builder.setOverheadWalltimeMilliseconds(
191 builder.getOverheadWalltimeMilliseconds() + overheadWalltimeMillis);
196 @Override
197 public List<RequestStatProto> getSummaries() {
198 List<Object> keys = new ArrayList<Object>(KEY_MODULUS);
199 for (int i = 0; i < KEY_MODULUS; i++) {
200 keys.add(makeKeyPrefix(i * KEY_DISTANCE) + PART_SUFFIX);
202 List<RequestStatProto> result = new ArrayList<RequestStatProto>();
203 for (Map.Entry<?, ?> entry : statsMemcache.getAll(keys).entrySet()) {
204 try {
205 result.add(RequestStatProto.newBuilder().mergeFrom(
206 (byte[]) entry.getValue()).build());
207 } catch (InvalidProtocolBufferException e) {
208 log.warning(
209 "Memcache store for request stats is partially corrupted for key "
210 + entry.getKey());
211 statsMemcache.delete(entry.getKey());
214 return result;
217 @Override
218 public RequestStatProto getFull(long timestamp) {
219 String key = makeKeyPrefix(timestamp) + FULL_SUFFIX;
220 try {
221 byte[] rawData = (byte[]) statsMemcache.get(key);
222 return (rawData == null) ? null :
223 RequestStatProto.newBuilder().mergeFrom(rawData).build();
224 } catch (InvalidProtocolBufferException e) {
225 log.warning(
226 "Memcache store for request stats is partially corrupted for key " + key);
227 statsMemcache.delete(key);
228 return null;
232 private void persist(RequestStatProto stats) {
233 RequestStatProto.Builder summary = RequestStatProto.newBuilder().mergeFrom(stats);
234 for (FieldDescriptor field : RequestStatProto.getDescriptor().getFields()) {
235 if (field.getNumber() > FIRST_FIELD_NUMBER_FOR_DETAILS) {
236 summary.clearField(field);
239 Map<Object, Object> values = new HashMap<Object, Object>();
240 String prefix = makeKeyPrefix(stats.getStartTimestampMilliseconds());
241 values.put(prefix + PART_SUFFIX, serialize(summary.build()));
242 values.put(prefix + FULL_SUFFIX, serialize(stats));
243 statsMemcache.putAll(values, Expiration.byDeltaSeconds(EXPIRATION_SECONDS));
246 byte[] serialize(RequestStatProto proto) {
247 byte[] result = serializeIfSmallEnough(proto);
248 if (result == null) {
249 proto = removeStackTraces(proto);
250 result = serializeIfSmallEnough(proto);
251 logMaybe(result, "all stack traces were removed");
253 if (result == null) {
254 proto = trimStatsEntries(100, proto);
255 result = serializeIfSmallEnough(proto);
256 logMaybe(result,
257 "trimmed the amount of individual stats down to 100 entries");
259 if (result == null) {
260 proto = trimStatsEntries(0, proto);
261 result = serializeIfSmallEnough(proto);
262 logMaybe(result,
263 "cleared out all individual stats");
265 if (result == null) {
266 throw new MemcacheServiceException("Appstats data too big");
268 return result;
271 private byte[] serializeIfSmallEnough(RequestStatProto proto) {
272 byte[] result = proto.toByteArray();
273 return (result.length > MAX_SIZE) ? null : result;
276 private void logMaybe(byte[] result, String action) {
277 if (result != null) {
278 log.warning("Stats data was too big, " + action + ".");
282 private RequestStatProto removeStackTraces(RequestStatProto proto) {
283 Builder builder = proto.toBuilder().clearIndividualStats();
284 for (IndividualRpcStatsProto stat : proto.getIndividualStatsList()) {
285 builder.addIndividualStats(stat.toBuilder().clearCallStack());
287 return builder.build();
290 private RequestStatProto trimStatsEntries(int max, RequestStatProto proto) {
291 Builder builder = proto.toBuilder().clearIndividualStats();
292 for (int i = 0; i < Math.min(max, proto.getIndividualStatsCount()); i++) {
293 builder.addIndividualStats(proto.getIndividualStats(i));
295 return builder.build();