1.9.30 sync.
[gae.git] / java / src / main / com / google / appengine / tools / appstats / MemcacheWriter.java
blobb98eee8803e1d94388c345beae1165f47304db8b
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.apphosting.api.ApiProxy.Delegate;
15 import com.google.apphosting.api.ApiProxy.Environment;
16 import com.google.common.collect.Maps;
17 import com.google.protobuf.Descriptors.FieldDescriptor;
18 import com.google.protobuf.InvalidProtocolBufferException;
20 import java.util.ArrayList;
21 import java.util.HashMap;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.logging.Level;
25 import java.util.logging.Logger;
27 import javax.servlet.http.HttpServletRequest;
29 /**
30 * Persists stats information in memcache. Can also be used to access that data
31 * again. This class is thread-safe, assuming that the underlying MemcacheService
32 * is.
35 public class MemcacheWriter implements Recorder.RecordWriter {
37 private static final int FIRST_FIELD_NUMBER_FOR_DETAILS = 100;
39 private static final String KEY_PREFIX = "__appstats__";
41 private static final String KEY_TEMPLATE = ":%06d";
43 private static final String PART_SUFFIX = ":part";
45 private static final String FULL_SUFFIX = ":full";
47 private static final int KEY_DISTANCE = 100;
49 private static final int KEY_MODULUS = 1000;
51 static final int MAX_SIZE = 1000000;
53 private static final int EXPIRATION_SECONDS = 36 * 3600;
55 public static final String STATS_NAMESPACE = "__appstats__";
57 private static String makeKeyPrefix(long timestamp) {
58 return String.format(
59 KEY_PREFIX + KEY_TEMPLATE,
60 ((timestamp / KEY_DISTANCE) % KEY_MODULUS) * KEY_DISTANCE);
63 protected final Logger log = Logger.getLogger(getClass().getName());
64 private final Recorder.Clock clock;
66 String keyInCache;
68 private final MemcacheService statsMemcache;
70 public MemcacheWriter(Clock clock, MemcacheService service) {
71 this.clock = clock;
72 this.keyInCache = getClass().getName() + ".CACHED_STATS";
73 this.statsMemcache = service;
74 if (service == null) {
75 throw new NullPointerException("Memcache service not found");
79 @Override
80 public final long begin(
81 Delegate<?> wrappedDelegate, Environment environment, HttpServletRequest request) {
82 long beganAt = clock.currentTimeMillis();
84 RequestStatProto.Builder builder = RequestStatProto.newBuilder();
85 builder.setStartTimestampMilliseconds(beganAt);
86 builder.setHttpMethod(request.getMethod());
87 builder.setHttpPath(request.getRequestURI());
88 String queryUnescaped = request.getQueryString();
89 if (queryUnescaped != null && queryUnescaped.length() > 0) {
90 builder.setHttpQuery("?" + queryUnescaped);
93 builder.setIsAdmin(environment.isAdmin());
94 if (environment.getEmail() != null) {
95 builder.setUserEmail(environment.getEmail());
98 builder.setOverheadWalltimeMilliseconds(clock.currentTimeMillis() - beganAt);
99 environment.getAttributes().put(keyInCache, builder);
100 return beganAt;
103 @Override
104 public final boolean commit(Delegate<?> wrappedDelegate, Environment environment,
105 int responseCode) {
107 RequestStatProto.Builder builder =
108 (RequestStatProto.Builder) environment.getAttributes().get(keyInCache);
109 if (builder == null) {
110 return false;
113 synchronized (builder) {
114 builder.setDurationMilliseconds(
115 clock.currentTimeMillis() - builder.getStartTimestampMilliseconds());
117 if (responseCode != 0) {
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;
169 @Override
170 public final void write(
171 Delegate<?> wrappedDelegate,
172 Environment environment,
173 IndividualRpcStatsProto.Builder record,
174 long overheadWalltimeMillis,
175 boolean correctStartOffset) {
176 if (record == null) {
177 throw new NullPointerException("Record must not be null");
179 if (environment == null) {
180 throw new NullPointerException("Environment must not be null");
182 RequestStatProto.Builder builder =
183 (RequestStatProto.Builder) environment.getAttributes().get(keyInCache);
184 if (builder != null) {
185 synchronized (builder) {
186 if (correctStartOffset) {
187 record.setStartOffsetMilliseconds(Math.max(0, record.getStartOffsetMilliseconds()
188 - builder.getStartTimestampMilliseconds()));
190 builder.addIndividualStats(record);
191 builder.setOverheadWalltimeMilliseconds(
192 builder.getOverheadWalltimeMilliseconds() + overheadWalltimeMillis);
197 @Override
198 public List<RequestStatProto> getSummaries() {
199 List<Object> keys = new ArrayList<Object>(KEY_MODULUS);
200 for (int i = 0; i < KEY_MODULUS; i++) {
201 keys.add(makeKeyPrefix(i * KEY_DISTANCE) + PART_SUFFIX);
203 List<RequestStatProto> result = new ArrayList<RequestStatProto>();
204 for (Map.Entry<?, ?> entry : statsMemcache.getAll(keys).entrySet()) {
205 try {
206 result.add(RequestStatProto.newBuilder().mergeFrom(
207 (byte[]) entry.getValue()).build());
208 } catch (InvalidProtocolBufferException e) {
209 log.warning(
210 "Memcache store for request stats is partially corrupted for key "
211 + entry.getKey());
212 statsMemcache.delete(entry.getKey());
215 return result;
218 @Override
219 public RequestStatProto getFull(long timestamp) {
220 String key = makeKeyPrefix(timestamp) + FULL_SUFFIX;
221 try {
222 byte[] rawData = (byte[]) statsMemcache.get(key);
223 return (rawData == null) ? null :
224 RequestStatProto.newBuilder().mergeFrom(rawData).build();
225 } catch (InvalidProtocolBufferException e) {
226 log.warning(
227 "Memcache store for request stats is partially corrupted for key " + key);
228 statsMemcache.delete(key);
229 return null;
233 private void persist(RequestStatProto stats) {
234 RequestStatProto.Builder summary = RequestStatProto.newBuilder().mergeFrom(stats);
235 for (FieldDescriptor field : RequestStatProto.getDescriptor().getFields()) {
236 if (field.getNumber() > FIRST_FIELD_NUMBER_FOR_DETAILS) {
237 summary.clearField(field);
240 Map<Object, Object> values = new HashMap<Object, Object>();
241 String prefix = makeKeyPrefix(stats.getStartTimestampMilliseconds());
242 values.put(prefix + PART_SUFFIX, serialize(summary.build()));
243 values.put(prefix + FULL_SUFFIX, serialize(stats));
244 statsMemcache.putAll(values, Expiration.byDeltaSeconds(EXPIRATION_SECONDS));
247 byte[] serialize(RequestStatProto proto) {
248 byte[] result = serializeIfSmallEnough(proto);
249 if (result == null) {
250 proto = removeStackTraces(proto);
251 result = serializeIfSmallEnough(proto);
252 logMaybe(result, "all stack traces were removed");
254 if (result == null) {
255 proto = trimStatsEntries(100, proto);
256 result = serializeIfSmallEnough(proto);
257 logMaybe(result,
258 "trimmed the amount of individual stats down to 100 entries");
260 if (result == null) {
261 proto = trimStatsEntries(0, proto);
262 result = serializeIfSmallEnough(proto);
263 logMaybe(result,
264 "cleared out all individual stats");
266 if (result == null) {
267 throw new MemcacheServiceException("Appstats data too big");
269 return result;
272 private byte[] serializeIfSmallEnough(RequestStatProto proto) {
273 byte[] result = proto.toByteArray();
274 return (result.length > MAX_SIZE) ? null : result;
277 private void logMaybe(byte[] result, String action) {
278 if (result != null) {
279 log.warning("Stats data was too big, " + action + ".");
283 private RequestStatProto removeStackTraces(RequestStatProto proto) {
284 RequestStatProto.Builder builder = proto.toBuilder().clearIndividualStats();
285 for (IndividualRpcStatsProto stat : proto.getIndividualStatsList()) {
286 builder.addIndividualStats(stat.toBuilder().clearCallStack());
288 return builder.build();
291 private RequestStatProto trimStatsEntries(int max, RequestStatProto proto) {
292 RequestStatProto.Builder builder = proto.toBuilder().clearIndividualStats();
293 for (int i = 0; i < Math.min(max, proto.getIndividualStatsCount()); i++) {
294 builder.addIndividualStats(proto.getIndividualStats(i));
296 return builder.build();