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
;
24 import java
.util
.logging
.Level
;
25 import java
.util
.logging
.Logger
;
27 import javax
.servlet
.http
.HttpServletRequest
;
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
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
) {
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
;
68 private final MemcacheService statsMemcache
;
70 public MemcacheWriter(Clock clock
, MemcacheService service
) {
72 this.keyInCache
= getClass().getName() + ".CACHED_STATS";
73 this.statsMemcache
= service
;
74 if (service
== null) {
75 throw new NullPointerException("Memcache service not found");
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
);
104 public final boolean commit(Delegate
<?
> wrappedDelegate
, Environment environment
,
107 RequestStatProto
.Builder builder
=
108 (RequestStatProto
.Builder
) environment
.getAttributes().get(keyInCache
);
109 if (builder
== null) {
113 synchronized (builder
) {
114 builder
.setDurationMilliseconds(
115 clock
.currentTimeMillis() - builder
.getStartTimestampMilliseconds());
117 if (responseCode
!= 0) {
118 builder
.setHttpStatus(responseCode
);
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
);
160 persist(builder
.build());
162 } catch (MemcacheServiceException e
) {
163 log
.log(Level
.WARNING
, "Error persisting request stats", e
);
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
);
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()) {
206 result
.add(RequestStatProto
.newBuilder().mergeFrom(
207 (byte[]) entry
.getValue()).build());
208 } catch (InvalidProtocolBufferException e
) {
210 "Memcache store for request stats is partially corrupted for key "
212 statsMemcache
.delete(entry
.getKey());
219 public RequestStatProto
getFull(long timestamp
) {
220 String key
= makeKeyPrefix(timestamp
) + FULL_SUFFIX
;
222 byte[] rawData
= (byte[]) statsMemcache
.get(key
);
223 return (rawData
== null) ?
null :
224 RequestStatProto
.newBuilder().mergeFrom(rawData
).build();
225 } catch (InvalidProtocolBufferException e
) {
227 "Memcache store for request stats is partially corrupted for key " + key
);
228 statsMemcache
.delete(key
);
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
);
258 "trimmed the amount of individual stats down to 100 entries");
260 if (result
== null) {
261 proto
= trimStatsEntries(0, proto
);
262 result
= serializeIfSmallEnough(proto
);
264 "cleared out all individual stats");
266 if (result
== null) {
267 throw new MemcacheServiceException("Appstats data too big");
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();