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
;
25 import java
.util
.logging
.Level
;
26 import java
.util
.logging
.Logger
;
28 import javax
.servlet
.http
.HttpServletRequest
;
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
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
) {
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
;
69 private final MemcacheService statsMemcache
;
71 public MemcacheWriter(Clock clock
, MemcacheService service
) {
73 this.keyInCache
= getClass().getName() + ".CACHED_STATS";
74 this.statsMemcache
= service
;
75 if (service
== null) {
76 throw new NullPointerException("Memcache service not found");
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
);
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) {
114 builder
.setDurationMilliseconds(
115 clock
.currentTimeMillis() - builder
.getStartTimestampMilliseconds());
117 if (responseCode
!= null) {
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
);
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
);
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()) {
205 result
.add(RequestStatProto
.newBuilder().mergeFrom(
206 (byte[]) entry
.getValue()).build());
207 } catch (InvalidProtocolBufferException e
) {
209 "Memcache store for request stats is partially corrupted for key "
211 statsMemcache
.delete(entry
.getKey());
218 public RequestStatProto
getFull(long timestamp
) {
219 String key
= makeKeyPrefix(timestamp
) + FULL_SUFFIX
;
221 byte[] rawData
= (byte[]) statsMemcache
.get(key
);
222 return (rawData
== null) ?
null :
223 RequestStatProto
.newBuilder().mergeFrom(rawData
).build();
224 } catch (InvalidProtocolBufferException e
) {
226 "Memcache store for request stats is partially corrupted for key " + key
);
227 statsMemcache
.delete(key
);
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
);
257 "trimmed the amount of individual stats down to 100 entries");
259 if (result
== null) {
260 proto
= trimStatsEntries(0, proto
);
261 result
= serializeIfSmallEnough(proto
);
263 "cleared out all individual stats");
265 if (result
== null) {
266 throw new MemcacheServiceException("Appstats data too big");
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();