├── .gitignore ├── .travis.yml ├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── googlecode │ └── objectify │ └── insight │ ├── Bucket.java │ ├── BucketFactory.java │ ├── BucketKey.java │ ├── BucketList.java │ ├── Clock.java │ ├── Codepointer.java │ ├── Collect.java │ ├── Collector.java │ ├── Flusher.java │ ├── InsightAsyncDatastoreService.java │ ├── InsightFilter.java │ ├── InsightIterable.java │ ├── InsightIterator.java │ ├── InsightList.java │ ├── InsightPreparedQuery.java │ ├── InsightQueryResultIterable.java │ ├── Operation.java │ ├── Recorder.java │ ├── puller │ ├── BigUploader.java │ ├── BucketAggregator.java │ ├── InsightDataset.java │ ├── Puller.java │ └── TablePicker.java │ ├── servlet │ ├── AbstractBigQueryServlet.java │ ├── AbstractPullerServlet.java │ ├── AbstractTableMakerServlet.java │ ├── GuicePullerServlet.java │ └── GuiceTableMakerServlet.java │ └── util │ ├── QueueHelper.java │ ├── ReflectionUtils.java │ ├── StackTraceUtils.java │ └── TaskHandleHelper.java └── test └── java └── com └── googlecode └── objectify └── insight ├── CodepointerTest.java ├── puller ├── TablePickerTest.java └── test │ └── PullerTest.java ├── test ├── FlusherTest.java ├── InsightAsyncDatastoreServiceTest.java ├── InsightCollectorTest.java ├── InsightCollectorTimeTest.java ├── InsightPreparedQueryTest.java ├── RecorderTest.java └── util │ ├── BucketsMatcher.java │ ├── FakeQueryResultList.java │ ├── PassThroughProxy.java │ └── TestBase.java └── util └── test └── StackTraceUtilsTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | target 3 | test-output 4 | .settings 5 | .project 6 | .classpath 7 | .idea 8 | *.iml 9 | .svn 10 | .gwt 11 | .gradle 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Objectify Insight 2 | 3 | This library provides insight into your high-volume GAE datastore activity. It records read and write activity 4 | broken down by time, namespace, module, version, kind, operation, and query and aggregates this data into Google BigQuery. 5 | By aggregating at multiple levels, Insight scales to thousands of requests per second. 6 | 7 | Insight works well with Google App Engine applications that use Objectify, but (with some limitations) it can work 8 | with any application that uses the low level datastore API. 9 | 10 | Insight is a metrics collection system. It flows aggregated data into BigQuery in a format that should be useful to 11 | developers and system administrators. It does not provide a query interface to BigQuery. 12 | 13 | ## Code 14 | 15 | https://github.com/stickfigure/objectify-insight 16 | 17 | https://github.com/stickfigure/objectify-insight-example 18 | 19 | ## Design 20 | 21 | Insight has several moving parts: 22 | 23 | * A facade of the low-level `AsyncDatastoreService` which aggregates metrics in instance memory and periodically 24 | flushes them to a pull queue. 25 | * A task, which should be called via cron (every minute) which aggregates pull queue tasks and pushes these 26 | aggregations into BigQuery. 27 | * A task, which should be called via cron (infrequently) which ensures that the appropriate BigQuery tables 28 | exist. 29 | 30 | The resulting BigQuery table data will look something like this: 31 | 32 | ``` 33 | | uploaded | codepoint | namespace | module | version | kind | op | query | time | reads | writes | 34 | | ----------------------- | -------------------------------- | ---------- | -------- | ------- | ------ | ------ | ------------------------------ | ----------------------- | ----- | ------ | 35 | | 2014-09-15 04:58:40 UTC | d41d8cd98f00b204e9800998ecf8427e | namespace2 | default | v1 | Thing1 | QUERY | SELECT * FROM Thing1 WHERE ... | 2014-09-15 04:58:40 UTC | 4 | 0 | 36 | | 2014-09-15 04:58:40 UTC | 9e107d9d372bb6826bd81d3542a419d6 | namespace1 | deferred | v1 | Thing2 | DELETE | | 2014-09-15 04:58:40 UTC | 0 | 1 | 37 | | 2014-09-15 04:58:40 UTC | e4d909c290d0fb1ca068ffaddf22cbd0 | namespace1 | default | v2 | Thing1 | SAVE | | 2014-09-15 04:58:40 UTC | 0 | 1 | 38 | ``` 39 | 40 | If you've ever seen a ROLAP database, this should look familiar. *codepoint*, *namespace*, *module*, *version*, *kind*, *op*, *query*, and *time* are 41 | dimensions; *reads* and *writes* are the aggregated statistics. 42 | 43 | *uploaded* is the date that the batch was uploaded to BigQuery. *time* is the actual date of the operation, 44 | rounded to a configurable boundary (default 1 minute) to allow for reasonable aggregation. 45 | 46 | *reads* and *writes* are entity counts, not operation counts. 47 | 48 | *codepoint* is the md5 hash of a stacktrace to the unique point in your code where the datastore operation took place. 49 | To look up the actual stacktrace, grep your App Engine logs for the hash value. Each instance will log the 50 | definition of each codepoint exactly once. Enable INFO logging at `com.googlecode.objectify.insight`. 51 | 52 | ## Installation 53 | 54 | If you use Guice, you may find it helpful to read the code at https://github.com/stickfigure/objectify-insight-example 55 | Guice is not required to use Insight, but it helps. This documentation assumes you will use Guice. 56 | 57 | ### Set up Queue 58 | 59 | Add a pull queue named "insight" to your `queue.xml`: 60 | 61 | ```xml 62 | 63 | 64 | insight 65 | pull 66 | 67 | 68 | ``` 69 | 70 | ### Set up Cron 71 | 72 | Add two entries to your `cron.xml`: 73 | 74 | ```xml 75 | 76 | 77 | /private/tableMaker 78 | Make sure we have enough tables for a week 79 | every 8 hours 80 | 81 | 82 | /private/puller 83 | Move all data to BQ 84 | every 1 minutes 85 | 86 | 87 | ``` 88 | 89 | ### Enable the servlets 90 | 91 | In your guice `ServletModule`, serve the paths you specified in `cron.xml` above with the relevant servlets: 92 | 93 | ```java 94 | serve("/private/tableMaker").with(GuiceTableMakerServlet.class); 95 | serve("/private/puller").with(GuicePullerServlet.class); 96 | ``` 97 | 98 | You will likely want to secure these servlets by using the standard security features in GAE: 99 | 100 | https://developers.google.com/appengine/docs/java/config/webxml#Security_and_Authentication 101 | 102 | It is not dangerous to expose these endpoints to the public, but they are not for human consumption. 103 | 104 | #### Servlets without Guice 105 | 106 | If you are not using Guice (or another JSR-330 compabile DI framework), extend the `AbstractTableMakerServlet` 107 | and `AbstractPullerServlet` classes. They offer a poor-man's DI system. 108 | 109 | ### Get an AsyncDatastoreService 110 | 111 | Insight is implemented as a wrapper to the GAE low-level API `AsyncDatastoreService` class. 112 | The `InsightAsyncDatastoreService` itself is constructed by passing in the raw `AsyncDatastoreService` 113 | you get from Google, plus the Insight `Recorder`. The `Recorder` requires the `Collector` and `BucketFactory`... etc. 114 | Guice (or any other JSR-330 compatible DI framework) makes this much more convenient and all pretty much automatic. 115 | 116 | Here is the minimum Guice configuration you would need to be able to get the `Recorder` 117 | out of the injector. That is, we want this to work: 118 | 119 | ```java 120 | AsyncDatastoreService raw = DatastoreServiceFactory.getAsyncDatastoreService(); 121 | Recorder recorder = injector.getInstance(Recorder.class); 122 | AsyncDatastoreService tracksMetrics = new InsightAsyncDatastoreService(raw, recorder); 123 | ``` 124 | 125 | These are the bindings you will need to create in your Guice module: 126 | 127 | ```java 128 | @Provides 129 | Bigquery bigquery() { 130 | // your complicated code to generate an authenticated connection here 131 | } 132 | 133 | /** The bigquery project and dataset ids where you will write data */ 134 | @Provides 135 | InsightDataset insightDataset() { 136 | return new InsightDataset() { 137 | @Override 138 | public String projectId() { 139 | return "objectify-insight-test"; 140 | } 141 | 142 | @Override 143 | public String datasetId() { 144 | return "insight_example"; 145 | } 146 | }; 147 | } 148 | 149 | /** There must be a Queue bound with the name "insight" */ 150 | @Provides 151 | @Named("insight") 152 | public Queue queue() { 153 | return QueueFactory.getQueue(Flusher.DEFAULT_QUEUE); 154 | } 155 | 156 | ``` 157 | 158 | Creating an authenticated instance of `Bigquery` is not in the scope of this document. If you make it injectible, 159 | Guice will inject it into Insight. Insight also needs to know the project and dataset ids for bigquery, and the 160 | pull queue that will be used for aggregation. 161 | 162 | ### [Decide what to record](#decide-what-to-record) 163 | 164 | By default, Insight ignores everything. You can tell the `Recorder` to record specific kinds or to record everything. 165 | `Recorder` is a singleton; this configuration only needs to happen once: 166 | 167 | ```java 168 | Recorder recorder = injector.getInstance(Recorder.class); 169 | 170 | // You can specify kinds individually 171 | recorder.recordKind("Thing"); 172 | recorder.recordKind("OtherThing"); 173 | 174 | // If true, all kinds will be recorded 175 | recorder.setRecordAll(true); 176 | ``` 177 | 178 | You can disable recording of codepoint hashes by calling: 179 | 180 | ```java 181 | recorder.getCodepointer().setDisabled(true); 182 | ``` 183 | 184 | ### Use Insight with Objectify 185 | 186 | Assuming you have injected the `Recorder` into your `ObjectifyFactory`, override these methods: 187 | 188 | #### ObjectifyFactory.createRawAsyncDatastoreService() 189 | 190 | The `ObjectifyFactory` uses an overridable method to obtain the low-level `AsyncDatastoreService` interface. 191 | Override this method and return your wrapper `InsightAsyncDatastoreService`: 192 | 193 | ```java 194 | @Override 195 | protected AsyncDatastoreService createRawAsyncDatastoreService(DatastoreServiceConfig cfg) { 196 | AsyncDatastoreService raw = super.createRawAsyncDatastoreService(cfg); 197 | return new InsightAsyncDatastoreService(raw, recorder); 198 | } 199 | ``` 200 | 201 | #### ObjectifyFactory.register() 202 | 203 | This allows you to use the `Collect` annotation on POJO entity classes to enable recording. This is an alternative 204 | to registering kinds one-at-a-time by hand. 205 | 206 | ```java 207 | @Override 208 | public void register(Class clazz) { 209 | super.register(clazz); 210 | 211 | if (clazz.isAnnotationPresent(Collect.class)) 212 | recorder.recordKind(Key.getKind(clazz)); 213 | } 214 | ``` 215 | 216 | This override can be skipped if you use `Recorder.setRecordAll(true)`. 217 | 218 | ## Configuration 219 | 220 | Insight has tunable parameters spread across several different singleton objects in the object graph. You can 221 | inject/fetch them in Guice and reset values, or (if you aren't using guice) set them as you construct the object 222 | graph manually. 223 | 224 | Broken down by object: 225 | 226 | ### Collector 227 | 228 | ```java 229 | Collector collector = injector.getInstance(Collector.class); 230 | collector.setSizeThreshold(500); 231 | collector.setAgeThresholdMillis(1000 * 30); 232 | ``` 233 | 234 | The Collector is responsible for aggregating metrics and periodically flushing aggregations to the `Flusher`. 235 | Flushing occurs when the number of separate aggregations exceeds a threshold, or the oldest bucket hits an 236 | age threshold. 237 | 238 | Note that age-threshold flushing occurs within the context of the next collection request; Insight does not 239 | create extra threads in your application. 240 | 241 | ### Clock 242 | 243 | ```java 244 | Clock clock = injector.getInstance(Clock.class); 245 | clock.setGranularityMillis(1000 * 600); 246 | ``` 247 | 248 | Most requests come in at fairly unique millisecond clock values. In order to get meaningful aggregation, 249 | we must 'round' clock values to something more granular. Coarser (higher) numbers provide better aggregation 250 | at the cost of less precisely knowing when activities happen. 251 | 252 | ### TablePicker 253 | 254 | ```java 255 | TablePicker picker = injector.getInstance(TablePicker.class); 256 | picker.setFormat(new SimpleDateFormat("'myprefix_'yyMMdd"); 257 | ``` 258 | 259 | You can change the format of table names; be sure to include any prefix as a constant in the DateFormat. 260 | 261 | ### Puller 262 | 263 | ```java 264 | Puller puller = injector.getInstance(Puller.class); 265 | puller.setBatchSize(50); 266 | puller.setLeaseDurationSeconds(300); 267 | ``` 268 | 269 | The Puller pulls batches of data off of the pull queue and pushes them to BigQuery. Since BigQuery is limited 270 | to how large a single request can be, you might need to adjust the batch size. The default is 20. If you get 271 | "request too large" errors, adjust this down. 272 | 273 | See https://github.com/stickfigure/objectify-insight/issues/3 274 | 275 | ### Codepointer 276 | 277 | As shown in the [previous section](#decide-what-to-record) codepoint generation can be completely disabled. 278 | 279 | Regardless if you decide to keep it enabled, you can tweak it further by replacing the ```StackProducer```. For example: 280 | ```java 281 | codepointer.setStackProducer(new AdvancedStackProducer()); 282 | ``` 283 | You can modify what to include in the stacktrace used to generate the codepoint. You can get rid of 284 | the irrelevant classes from the plaform, filters, servlets, package names can be abbreviated, etc. 285 | 286 | This affects the stacktrace dump seen once per instance, but it can be also achieved 287 | that a refactor - or an addition of a new filter - won't change every codepoint you have. 288 | 289 | ##### LegacyStackProducer 290 | Uses the pre-1.0.5 behaviour, which keeps the stacktrace intact, and only removes the mutable parts from generated classes' names. 291 | 292 | ##### AdvancedStackProducer 293 | Removes every stacktrace element that is irrelevant. The resulting stacktrace should only contain business-wise important lines. 294 | 295 | For example it removes: platform-specific servlets/filters, proxy/reflection related classes, ```guice``` injection related classes, 296 | ```endpoints-java``` related classes, ```gwt-rpc``` related classes, ```objectify```/```objectify-insight``` related classes, etc. 297 | 298 | Although you can subclass ```FilteringStackProducer``` and override ```filterStack(Iterable stack)``` 299 | on your own - which is the superclass of ```AdvancedStackProducer``` -, but most likely you can achieve the best result 300 | by using - or if you need any custom filtering by extending - this class. 301 | 302 | ## Limitations 303 | 304 | Insight tracks all of the datastore operations used by Objectify, but does not track every operation you can 305 | possibly perform in the low-level API. In particular, it is possible to trigger read operations on `List` 306 | objects in such a way that Insight cannot determine statistics without potentially impacting the performance 307 | of your application. For example: 308 | 309 | ```java 310 | PreparedQuery pq = ds.prepare(query); 311 | List entities = pq.asList(fetchOpts).asList(); 312 | int size = entities.size(); 313 | ``` 314 | 315 | Insight doesn't know what to do with this without explicitly iterating through the `List`. 316 | 317 | As long as you iterate through results in the low-level API at least once, Insight will track statistics. 318 | Note that if you use Objectify, this limitation does not apply; Objectify always iterates the original `List`. 319 | 320 | ## More 321 | 322 | If you have questions, ask on the Objectify Google Group: 323 | 324 | http://groups.google.com/group/objectify-appengine 325 | 326 | ## License 327 | 328 | Released under the MIT License. 329 | 330 | ## Thanks 331 | 332 | Huge thanks to BetterCloud (http://www.bettercloud.com/) for funding this project! 333 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | 1.9.64 7 | UTF-8 8 | 9 | 10 | com.googlecode.objectify 11 | objectify-insight 12 | 1.0.6-SNAPSHOT 13 | 14 | Objectify App Engine Insight 15 | Insight into your high-volume datastore behavior 16 | jar 17 | https://github.com/objectify/objectify-insight 18 | 19 | scm:git:https://github.com/stickfigure/objectify-insight.git 20 | scm:git:git@github.com:stickfigure/objectify-insight.git 21 | HEAD 22 | 23 | 24 | 25 | MIT License 26 | http://www.opensource.org/licenses/mit-license.php 27 | 28 | 29 | 30 | 31 | jeff 32 | Jeff Schnitzer 33 | jeff@infohazard.org 34 | 35 | 36 | 37 | 38 | 39 | ossrh 40 | https://oss.sonatype.org/content/repositories/snapshots 41 | 42 | 43 | ossrh 44 | Nexus Release Repository 45 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 46 | 47 | 48 | 49 | 50 | 51 | release-sign-artifacts 52 | 53 | 54 | performRelease 55 | true 56 | 57 | 58 | 59 | 60 | 61 | org.apache.maven.plugins 62 | maven-gpg-plugin 63 | 64 | 65 | sign-artifacts 66 | verify 67 | 68 | sign 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | maven-compiler-plugin 82 | 3.7.0 83 | 84 | 1.8 85 | 1.8 86 | 87 | 88 | 89 | org.apache.maven.plugins 90 | maven-source-plugin 91 | 3.0.1 92 | 93 | 94 | 95 | jar 96 | 97 | 98 | 99 | 100 | 101 | 102 | org.apache.maven.plugins 103 | maven-surefire-plugin 104 | 2.22.0 105 | 106 | 107 | **/*Test*.java 108 | 109 | true 110 | random 111 | 112 | 113 | 114 | 115 | org.sonatype.plugins 116 | nexus-staging-maven-plugin 117 | 1.6.5 118 | true 119 | 120 | ossrh 121 | https://oss.sonatype.org/ 122 | true 123 | 124 | 125 | 126 | 127 | org.apache.maven.plugins 128 | maven-release-plugin 129 | 2.5.1 130 | 131 | true 132 | release-sign-artifacts 133 | 134 | 135 | 136 | 137 | org.apache.maven.plugins 138 | maven-javadoc-plugin 139 | 2.10.1 140 | 141 | 142 | 143 | jar 144 | 145 | 146 | 147 | 148 | -Xdoclint:none 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | javax.inject 157 | javax.inject 158 | 1 159 | 160 | 161 | 162 | com.google.guava 163 | guava 164 | 25.1-jre 165 | 166 | 167 | 168 | com.fasterxml.jackson.core 169 | jackson-core 170 | 2.8.4 171 | 172 | 173 | com.fasterxml.jackson.core 174 | jackson-databind 175 | 2.8.4 176 | 177 | 178 | 179 | com.google.apis 180 | google-api-services-bigquery 181 | v2-rev383-1.23.0 182 | 183 | 184 | 185 | 186 | com.google.appengine 187 | appengine-api-1.0-sdk 188 | ${gae.version} 189 | provided 190 | 191 | 192 | com.google.appengine 193 | appengine-api-labs 194 | ${gae.version} 195 | provided 196 | 197 | 198 | com.google.appengine 199 | appengine-api-stubs 200 | ${gae.version} 201 | test 202 | 203 | 204 | com.google.appengine 205 | appengine-testing 206 | ${gae.version} 207 | test 208 | 209 | 210 | 211 | javax.servlet 212 | servlet-api 213 | 2.5 214 | provided 215 | 216 | 217 | 218 | 219 | org.testng 220 | testng 221 | 6.8 222 | test 223 | 224 | 225 | hamcrest-core 226 | org.hamcrest 227 | 228 | 229 | 230 | 231 | 232 | org.mockito 233 | mockito-core 234 | 1.9.5 235 | test 236 | 237 | 238 | hamcrest-core 239 | org.hamcrest 240 | 241 | 242 | 243 | 244 | 245 | org.hamcrest 246 | hamcrest-library 247 | 1.3 248 | test 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | org.projectlombok 268 | lombok 269 | 1.16.20 270 | provided 271 | 272 | 273 | 274 | 275 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/Bucket.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | /** 8 | * One bucket of data we aggregate to. 9 | */ 10 | @Data 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class Bucket { 14 | /** 15 | */ 16 | private BucketKey key; 17 | 18 | /** Variable data that is aggregated */ 19 | private long reads; 20 | private long writes; 21 | 22 | /** */ 23 | public Bucket(BucketKey key) { 24 | this.key = key; 25 | } 26 | 27 | /** Merge the other bucket into this one */ 28 | public void merge(Bucket other) { 29 | reads += other.getReads(); 30 | writes += other.getWrites(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/BucketFactory.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight; 2 | 3 | import lombok.Data; 4 | 5 | import javax.annotation.Nullable; 6 | import javax.inject.Inject; 7 | import javax.inject.Named; 8 | 9 | import com.google.appengine.api.modules.ModulesServiceFactory; 10 | 11 | /** 12 | * Buckets are given a timestamp that depends on a configurable value, so we need to 13 | * make them from a factory. 14 | */ 15 | @Data 16 | public class BucketFactory { 17 | 18 | /** Gives us rounded timestamps */ 19 | private final Clock clock; 20 | private final String module; 21 | private final String version; 22 | 23 | public BucketFactory() { 24 | this(new Clock(), ModulesServiceFactory.getModulesService().getCurrentModule(), ModulesServiceFactory.getModulesService().getCurrentVersion()); 25 | } 26 | 27 | @Inject 28 | public BucketFactory(Clock clock, @Named("module") String module, @Named("version") String version) { 29 | this.clock = clock; 30 | this.module = module; 31 | this.version = version; 32 | } 33 | 34 | /** 35 | */ 36 | public Bucket forGet(String codePoint, String namespace, String kind, long readCount) { 37 | return new Bucket(new BucketKey(codePoint, namespace, module, version, kind, Operation.GET, null, clock.getTime()), readCount, 0); 38 | } 39 | 40 | /** 41 | */ 42 | public Bucket forPut(String codePoint, String namespace, String kind, boolean insert, long writeCount) { 43 | Operation op = insert ? Operation.INSERT : Operation.UPDATE; 44 | return new Bucket(new BucketKey(codePoint, namespace, module, version, kind, op, null, clock.getTime()), 0, writeCount); 45 | } 46 | 47 | /** 48 | */ 49 | public Bucket forDelete(String codePoint, String namespace, String kind, long writeCount) { 50 | return new Bucket(new BucketKey(codePoint, namespace, module, version, kind, Operation.DELETE, null, clock.getTime()), 0, writeCount); 51 | } 52 | 53 | /** 54 | */ 55 | public Bucket forQuery(String codePoint, String namespace, String kind, String queryString, long readCount) { 56 | return new Bucket(new BucketKey(codePoint, namespace, module, version, kind, Operation.QUERY, queryString, clock.getTime()), readCount, 0); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/BucketKey.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | /** 8 | * Immutable key that identifies a bucket (one vertex of the hypercube). 9 | * Wish everything could be final but then Jackson can't deserialize it. 10 | */ 11 | @Data 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class BucketKey { 15 | private String codepoint; 16 | private String namespace; 17 | private String module; 18 | private String version; 19 | private String kind; 20 | private Operation op; 21 | private String query; 22 | 23 | /** 24 | * Time is a currentTimeMillis() rounded to a block edge for aggregation. 25 | */ 26 | private long time; 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/BucketList.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import java.util.ArrayList; 7 | import java.util.Collection; 8 | import java.util.List; 9 | 10 | /** 11 | * All we really need to serialize today is a List of buckets. However, it's bad for future extensibility 12 | * to have an array as the top-level object in our JSON structure, so we wrap it with an object. If we 13 | * ever need to add fields, we can do it without invalidating any data stored in the task queue. 14 | */ 15 | @Data 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | public class BucketList { 19 | List buckets; 20 | 21 | public BucketList(Collection buckets) { 22 | this.buckets = new ArrayList<>(buckets); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/Clock.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight; 2 | 3 | import lombok.Data; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import lombok.extern.java.Log; 7 | import javax.inject.Singleton; 8 | 9 | /** 10 | * Gets the current time, rounded to whatever arbitrary boundry we want. 11 | */ 12 | @Log 13 | @Singleton 14 | @Data 15 | public class Clock { 16 | 17 | /** 18 | * Aggregate events into time buckets with this granularity. What this effectively means 19 | * is that all timestamps are rounded by this amount. 20 | */ 21 | @Getter @Setter 22 | private long granularityMillis = 60 * 1000; 23 | 24 | /** 25 | * Get the current time value rounded to the specified boundary 26 | */ 27 | public long getTime() { 28 | long time = System.currentTimeMillis(); 29 | return time - time % granularityMillis; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/Codepointer.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight; 2 | 3 | import java.io.PrintWriter; 4 | import java.io.StringWriter; 5 | import java.io.UnsupportedEncodingException; 6 | import java.security.MessageDigest; 7 | import java.security.NoSuchAlgorithmException; 8 | import java.util.Arrays; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | 11 | import javax.inject.Singleton; 12 | 13 | import com.google.common.annotations.VisibleForTesting; 14 | import com.google.common.base.Predicate; 15 | import com.google.common.collect.FluentIterable; 16 | import com.google.common.io.BaseEncoding; 17 | import com.googlecode.objectify.insight.util.StackTraceUtils; 18 | 19 | import lombok.Getter; 20 | import lombok.Setter; 21 | import lombok.experimental.Accessors; 22 | import lombok.extern.java.Log; 23 | 24 | /** 25 | * Identifies codepoints. Also logs codepoints which have never been seen before so that developers 26 | * can simply grep logs for the codepoint hash. It'll be in the logs somewhere. 27 | */ 28 | @Singleton 29 | @Log 30 | @Accessors(chain=true) 31 | public class Codepointer { 32 | 33 | /** If set true, we will not record code points - they will all be empty strings */ 34 | @Getter @Setter 35 | private boolean disabled; 36 | 37 | @Getter @Setter 38 | private StackProducer stackProducer = new LegacyStackProducer(); 39 | 40 | /** Track which ones we've logged already. It's a Set, just map to the key value */ 41 | private ConcurrentHashMap logged = new ConcurrentHashMap<>(); 42 | 43 | /** 44 | * Get the hash of the code point. Also logs the definition of the code point, once per codepoint (per instance). 45 | */ 46 | public String getCodepoint() { 47 | if (disabled) 48 | return "disabled"; 49 | 50 | String stack = stack(); 51 | String digest = digest(stack); 52 | 53 | if (logged.putIfAbsent(digest, digest) == null) { 54 | log.info("Codepoint " + digest + " is " + stack); 55 | } 56 | 57 | return digest; 58 | } 59 | 60 | @VisibleForTesting 61 | String stack() { 62 | return stackProducer.getStack(); 63 | } 64 | 65 | /** Give a hex encoded digest of the string */ 66 | @VisibleForTesting 67 | String digest(String str) { 68 | // Checked exceptions are retarded 69 | 70 | final MessageDigest md; 71 | try { 72 | md = MessageDigest.getInstance("MD5"); 73 | } catch (NoSuchAlgorithmException e) { 74 | throw new RuntimeException("Impossible", e); 75 | } 76 | 77 | try { 78 | return BaseEncoding.base16().encode(md.digest(str.getBytes("UTF-8"))); 79 | } catch (UnsupportedEncodingException e) { 80 | throw new RuntimeException("Impossible", e); 81 | } 82 | } 83 | 84 | public abstract static class StackProducer { 85 | protected abstract String getStack(); 86 | } 87 | 88 | public static class LegacyStackProducer extends StackProducer { 89 | @Override 90 | protected String getStack() { 91 | // It's tempting to getStackTrace() so we can skip all the Insight noise, but that would 92 | // clone the stacktrace which seems like extra gc work. 93 | StringWriter stackWriter = new StringWriter(1024); 94 | new Exception().printStackTrace(new PrintWriter(stackWriter)); 95 | String stack = stackWriter.toString(); 96 | stack = StackTraceUtils.removeMutableEnhancements(stack); 97 | return stack; 98 | } 99 | } 100 | 101 | public static abstract class FilteringStackProducer extends StackProducer { 102 | @Override 103 | protected String getStack() { 104 | Exception e = new Exception(); 105 | Iterable stackTrace = filterStack(Arrays.asList(e.getStackTrace())); 106 | 107 | StringBuilder sb = new StringBuilder(1024); 108 | sb.append(e).append("\r\n"); 109 | for (StackTraceElement ste : stackTrace) { 110 | sb.append("\tat ").append(ste.toString()).append("\r\n"); 111 | } 112 | return sb.toString(); 113 | } 114 | 115 | /** 116 | * Implement if you want to modify the stacktrace used for codepoint generation.
117 | * Useful for removing meaningless stack trace elements (for example: servlets, filters, etc). 118 | */ 119 | protected abstract Iterable filterStack(Iterable stack); 120 | 121 | /** 122 | * Removes stack trace elements from classes whose name - not simple name (!) - starts with the provided prefix. 123 | */ 124 | protected static Predicate removeStackTraceElementsFromPackage(final String classNamePrefix) { 125 | return new Predicate() { 126 | @Override 127 | public boolean apply(StackTraceElement ste) { 128 | return !ste.getClassName().startsWith(classNamePrefix); 129 | } 130 | }; 131 | } 132 | 133 | protected static Predicate removeStackTraceElementsWithMutableEnhancements() { 134 | return new Predicate() { 135 | @Override 136 | public boolean apply(StackTraceElement ste) { 137 | return !StackTraceUtils.containsMutableEnhancements(ste.getClassName()); 138 | } 139 | }; 140 | } 141 | } 142 | 143 | public static class AdvancedStackProducer extends FilteringStackProducer { 144 | @Override 145 | protected Iterable filterStack(Iterable stack) { 146 | return FluentIterable.from(stack) 147 | // mutable enhancements 148 | .filter(removeStackTraceElementsWithMutableEnhancements()) 149 | // basic appengine servlet classes 150 | .filter(removeStackTraceElementsFromPackage("com.google.apphosting.")) 151 | .filter(removeStackTraceElementsFromPackage("com.google.tracing.")) 152 | .filter(removeStackTraceElementsFromPackage("java.lang.Thread")) 153 | .filter(removeStackTraceElementsFromPackage("javax.servlet.http.")) 154 | .filter(removeStackTraceElementsFromPackage("org.mortbay.jetty.")) 155 | .filter(removeStackTraceElementsFromPackage("org.eclipse.jetty.")) 156 | // proxy and reflection classes 157 | .filter(removeStackTraceElementsFromPackage("com.sun.proxy.")) 158 | .filter(removeStackTraceElementsFromPackage("java.lang.reflect.")) 159 | .filter(removeStackTraceElementsFromPackage("java.security.")) 160 | .filter(removeStackTraceElementsFromPackage("sun.reflect.")) 161 | // guice 162 | .filter(removeStackTraceElementsFromPackage("com.google.inject.")) 163 | // cloud endpoints 164 | .filter(removeStackTraceElementsFromPackage("com.google.api.server.spi.")) 165 | // gwt rpc 166 | .filter(removeStackTraceElementsFromPackage("com.google.gwt.user.server.rpc.")) 167 | // objectify, objectify-insight 168 | .filter(removeStackTraceElementsFromPackage("com.googlecode.objectify.")); 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/Collect.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Marker annotation that can be used in ObjectifyFactory.register() to determine whether a 10 | * kind should be registered with the Recorder. 11 | */ 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Target(ElementType.TYPE) 14 | public @interface Collect { 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/Collector.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import lombok.extern.java.Log; 6 | import javax.inject.Inject; 7 | import javax.inject.Singleton; 8 | import java.util.Date; 9 | import java.util.LinkedHashMap; 10 | import java.util.Map; 11 | import java.util.logging.Level; 12 | 13 | /** 14 | * Aggregates statistics and flushes them to a pull queue as necessary. Thread-safe. You should 15 | * create just one of these per application and pass it into all InsightAsyncDatastoreService instances. 16 | */ 17 | @Log 18 | @Singleton 19 | public class Collector { 20 | 21 | /** Where we flush statistics when we cross thresholds */ 22 | @Getter 23 | private final Flusher flusher; 24 | 25 | /** Date at which the first bucket as added; null if empty */ 26 | private Date oldest; 27 | 28 | /** 29 | * Maximum number of unique buckets before we flush. Since all buckets get JSONified into a single 30 | * queue task, the hard limit for this is determined by the max size of a task (1 MB). 31 | */ 32 | @Getter @Setter 33 | private int sizeThreshold = 1000; 34 | 35 | /** Maximum age of a bucket before we flush */ 36 | @Getter @Setter 37 | private long ageThresholdMillis = 30 * 1000; 38 | 39 | /** 40 | * Buckets we aggregate into; this is recreated every flush and instantiated lazily. 41 | * Use the buckets() method internally. 42 | */ 43 | private Map lazyBuckets; 44 | 45 | /** 46 | * Use the standard Flusher & Clock 47 | */ 48 | public Collector() { 49 | this(new Flusher()); 50 | } 51 | 52 | /** 53 | */ 54 | @Inject 55 | public Collector(Flusher flusher) { 56 | this.flusher = flusher; 57 | } 58 | 59 | /** Use this instead of referencing the lazy var explicitly */ 60 | private Map buckets() { 61 | if (lazyBuckets == null) 62 | lazyBuckets = new LinkedHashMap<>(sizeThreshold + sizeThreshold / 3); // what guava uses for best guess 63 | 64 | return lazyBuckets; 65 | } 66 | 67 | /** 68 | * Collect some statistics. The bucket will be merged with an equivalent bucket. 69 | * 70 | * @param bucket 71 | */ 72 | public synchronized void collect(Bucket bucket) { 73 | if (log.isLoggable(Level.FINEST)) 74 | log.finest("Collecting bucket " + bucket); 75 | 76 | if (oldest == null) 77 | oldest = new Date(); 78 | 79 | Bucket existing = buckets().get(bucket.getKey()); 80 | if (existing == null) { 81 | existing = new Bucket(bucket.getKey()); 82 | buckets().put(existing.getKey(), existing); 83 | } 84 | 85 | existing.merge(bucket); 86 | 87 | // Check time before size because flush() wipes out time 88 | if (oldest.getTime() + ageThresholdMillis <= System.currentTimeMillis()) 89 | flush(); 90 | 91 | if (buckets().size() >= sizeThreshold) 92 | flush(); 93 | } 94 | 95 | /** Flush the accumulated statistics and reset the collection. */ 96 | private void flush() { 97 | flusher.flush(buckets().values()); 98 | lazyBuckets = null; 99 | oldest = null; 100 | } 101 | 102 | /** 103 | * Only present for testing and debugging 104 | */ 105 | public synchronized Bucket getBucket(BucketKey bucketKey) { 106 | return buckets().get(bucketKey); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/Flusher.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight; 2 | 3 | import com.google.appengine.api.taskqueue.Queue; 4 | import com.google.appengine.api.taskqueue.QueueFactory; 5 | import com.googlecode.objectify.insight.util.QueueHelper; 6 | import lombok.extern.java.Log; 7 | import javax.inject.Inject; 8 | import javax.inject.Named; 9 | import javax.inject.Singleton; 10 | import java.util.Collection; 11 | 12 | /** 13 | * Writes aggregated statistics to a pull queue. 14 | */ 15 | @Log 16 | @Singleton 17 | public class Flusher { 18 | 19 | /** The default pull queue that statistics are flushed to */ 20 | public static final String DEFAULT_QUEUE = "insight"; 21 | 22 | /** The queue we use */ 23 | private QueueHelper queue; 24 | 25 | /** Use the default pull queue name */ 26 | public Flusher() { 27 | this(QueueFactory.getQueue(DEFAULT_QUEUE)); 28 | } 29 | 30 | /** */ 31 | @Inject 32 | public Flusher(@Named("insight") Queue queue) { 33 | this.setQueue(queue); 34 | } 35 | 36 | /** 37 | * Change the queue we flush to. 38 | */ 39 | public void setQueue(Queue queue) { 40 | this.queue = new QueueHelper(queue, BucketList.class); 41 | } 42 | 43 | /** 44 | * Write buckets to the relevant pull queue as a single task with a JSON payload. 45 | */ 46 | public void flush(Collection buckets) { 47 | log.finer("Flushing " + buckets.size() + " buckets to the queue"); 48 | 49 | queue.add(new BucketList(buckets)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/InsightAsyncDatastoreService.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight; 2 | 3 | import com.google.appengine.api.datastore.AsyncDatastoreService; 4 | import com.google.appengine.api.datastore.DatastoreAttributes; 5 | import com.google.appengine.api.datastore.Entity; 6 | import com.google.appengine.api.datastore.Index; 7 | import com.google.appengine.api.datastore.Key; 8 | import com.google.appengine.api.datastore.KeyRange; 9 | import com.google.appengine.api.datastore.PreparedQuery; 10 | import com.google.appengine.api.datastore.Query; 11 | import com.google.appengine.api.datastore.Transaction; 12 | import com.google.appengine.api.datastore.TransactionOptions; 13 | import com.googlecode.objectify.insight.Recorder.Batch; 14 | import com.googlecode.objectify.insight.Recorder.QueryBatch; 15 | import javax.inject.Inject; 16 | import javax.inject.Named; 17 | import java.util.Collection; 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.concurrent.Future; 21 | 22 | /** 23 | */ 24 | public class InsightAsyncDatastoreService implements AsyncDatastoreService { 25 | 26 | private final AsyncDatastoreService raw; 27 | 28 | private final Recorder recorder; 29 | 30 | @Inject 31 | public InsightAsyncDatastoreService(@Named("raw") AsyncDatastoreService raw, Recorder recorder) { 32 | this.raw = raw; 33 | this.recorder = recorder; 34 | } 35 | 36 | @Override 37 | public Future get(Key key) { 38 | recorder.batch().get(key); 39 | return raw.get(key); 40 | } 41 | 42 | @Override 43 | public Future get(Transaction transaction, Key key) { 44 | recorder.batch().get(key); 45 | return raw.get(transaction, key); 46 | } 47 | 48 | @Override 49 | public Future> get(Iterable keys) { 50 | Batch batch = recorder.batch(); 51 | for (Key key: keys) 52 | batch.get(key); 53 | 54 | return raw.get(keys); 55 | } 56 | 57 | @Override 58 | public Future> get(Transaction transaction, Iterable keys) { 59 | Batch batch = recorder.batch(); 60 | for (Key key: keys) 61 | batch.get(key); 62 | 63 | return raw.get(transaction, keys); 64 | } 65 | 66 | @Override 67 | public Future put(Entity entity) { 68 | recorder.batch().put(entity); 69 | return raw.put(entity); 70 | } 71 | 72 | @Override 73 | public Future put(Transaction transaction, Entity entity) { 74 | recorder.batch().put(entity); 75 | return raw.put(transaction, entity); 76 | } 77 | 78 | @Override 79 | public Future> put(Iterable entities) { 80 | Batch batch = recorder.batch(); 81 | for (Entity entity: entities) 82 | batch.put(entity); 83 | 84 | return raw.put(entities); 85 | } 86 | 87 | @Override 88 | public Future> put(Transaction transaction, Iterable entities) { 89 | Batch batch = recorder.batch(); 90 | for (Entity entity: entities) 91 | batch.put(entity); 92 | 93 | return raw.put(transaction, entities); 94 | } 95 | 96 | @Override 97 | public Future delete(Key... keys) { 98 | Batch batch = recorder.batch(); 99 | for (Key key: keys) 100 | batch.delete(key); 101 | 102 | return raw.delete(keys); 103 | } 104 | 105 | @Override 106 | public Future delete(Transaction transaction, Key... keys) { 107 | Batch batch = recorder.batch(); 108 | for (Key key: keys) 109 | batch.delete(key); 110 | 111 | return raw.delete(transaction, keys); 112 | } 113 | 114 | @Override 115 | public Future delete(Iterable keys) { 116 | Batch batch = recorder.batch(); 117 | for (Key key: keys) 118 | batch.delete(key); 119 | 120 | return raw.delete(keys); 121 | } 122 | 123 | @Override 124 | public Future delete(Transaction transaction, Iterable keys) { 125 | Batch batch = recorder.batch(); 126 | for (Key key: keys) 127 | batch.delete(key); 128 | 129 | return raw.delete(transaction, keys); 130 | } 131 | 132 | @Override 133 | public Future beginTransaction() { 134 | return raw.beginTransaction(); 135 | } 136 | 137 | @Override 138 | public Future beginTransaction(TransactionOptions transactionOptions) { 139 | return raw.beginTransaction(transactionOptions); 140 | } 141 | 142 | @Override 143 | public Future allocateIds(String s, long l) { 144 | return raw.allocateIds(s, l); 145 | } 146 | 147 | @Override 148 | public Future allocateIds(Key key, String s, long l) { 149 | return raw.allocateIds(key, s, l); 150 | } 151 | 152 | @Override 153 | public Future getDatastoreAttributes() { 154 | return raw.getDatastoreAttributes(); 155 | } 156 | 157 | @Override 158 | public Future> getIndexes() { 159 | return raw.getIndexes(); 160 | } 161 | 162 | @Override 163 | public PreparedQuery prepare(Query query) { 164 | PreparedQuery pq = raw.prepare(query); 165 | 166 | if (!query.isKeysOnly()) { 167 | QueryBatch batch = recorder.query(query); 168 | pq = new InsightPreparedQuery(pq, batch); 169 | } 170 | 171 | return pq; 172 | } 173 | 174 | @Override 175 | public PreparedQuery prepare(Transaction transaction, Query query) { 176 | PreparedQuery pq = raw.prepare(transaction, query); 177 | 178 | if (!query.isKeysOnly()) { 179 | QueryBatch batch = recorder.query(query); 180 | pq = new InsightPreparedQuery(pq, batch); 181 | } 182 | 183 | return pq; 184 | } 185 | 186 | @Override 187 | public Transaction getCurrentTransaction() { 188 | return raw.getCurrentTransaction(); 189 | } 190 | 191 | @Override 192 | public Transaction getCurrentTransaction(Transaction transaction) { 193 | return raw.getCurrentTransaction(transaction); 194 | } 195 | 196 | @Override 197 | public Collection getActiveTransactions() { 198 | return raw.getActiveTransactions(); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/InsightFilter.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight; 2 | 3 | import javax.servlet.Filter; 4 | import javax.servlet.FilterChain; 5 | import javax.servlet.FilterConfig; 6 | import javax.servlet.ServletException; 7 | import javax.servlet.ServletRequest; 8 | import javax.servlet.ServletResponse; 9 | import java.io.IOException; 10 | 11 | /** 12 | */ 13 | public class InsightFilter implements Filter { 14 | @Override 15 | public void init(FilterConfig filterConfig) throws ServletException { 16 | } 17 | 18 | @Override 19 | public void destroy() { 20 | } 21 | 22 | @Override 23 | public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/InsightIterable.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight; 2 | 3 | import com.google.appengine.api.datastore.Entity; 4 | import com.googlecode.objectify.insight.Recorder.QueryBatch; 5 | import lombok.RequiredArgsConstructor; 6 | import java.util.Iterator; 7 | 8 | /** 9 | */ 10 | @RequiredArgsConstructor 11 | public class InsightIterable implements Iterable { 12 | 13 | private final Iterable raw; 14 | 15 | protected final QueryBatch recorderBatch; 16 | 17 | private boolean collected; 18 | 19 | @Override 20 | public Iterator iterator() { 21 | if (collected) { 22 | return raw.iterator(); 23 | } else { 24 | collected = true; 25 | return InsightIterator.create(raw.iterator(), recorderBatch); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/InsightIterator.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight; 2 | 3 | import com.google.appengine.api.datastore.Entity; 4 | import com.google.appengine.api.datastore.QueryResultIterator; 5 | import com.googlecode.objectify.insight.Recorder.QueryBatch; 6 | import com.googlecode.objectify.insight.util.ReflectionUtils; 7 | import lombok.RequiredArgsConstructor; 8 | import java.lang.reflect.InvocationHandler; 9 | import java.lang.reflect.Method; 10 | import java.lang.reflect.Proxy; 11 | import java.util.Iterator; 12 | import java.util.ListIterator; 13 | 14 | /** 15 | * Dynamic proxy which covers any kind of iterator we might run across. 16 | */ 17 | @RequiredArgsConstructor 18 | public class InsightIterator implements InvocationHandler { 19 | 20 | public interface Interface extends QueryResultIterator, ListIterator {} 21 | 22 | private static final Method NEXT_METHOD = ReflectionUtils.getMethod(Iterator.class, "next"); 23 | private static final Method PREVIOUS_METHOD = ReflectionUtils.getMethod(ListIterator.class, "previous"); 24 | 25 | private final Object raw; 26 | 27 | private final QueryBatch recorderBatch; 28 | 29 | @Override 30 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 31 | Object result = method.invoke(raw, args); 32 | 33 | if (method.equals(NEXT_METHOD) || method.equals(PREVIOUS_METHOD)) { 34 | recorderBatch.query((Entity)result); 35 | } 36 | 37 | return result; 38 | } 39 | 40 | public static Interface create(Iterator raw, QueryBatch recorderBatch) { 41 | return (Interface)Proxy.newProxyInstance( 42 | Interface.class.getClassLoader(), 43 | new Class[]{Interface.class}, 44 | new InsightIterator(raw, recorderBatch)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/InsightList.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight; 2 | 3 | import com.google.appengine.api.datastore.Cursor; 4 | import com.google.appengine.api.datastore.Entity; 5 | import com.google.appengine.api.datastore.Index; 6 | import com.google.appengine.api.datastore.QueryResultList; 7 | import com.googlecode.objectify.insight.Recorder.QueryBatch; 8 | import java.util.Collection; 9 | import java.util.List; 10 | import java.util.ListIterator; 11 | 12 | /** 13 | * This doesn't count every form of access - it only handles iterator() and toArray(). If 14 | * you use get() to random-access the list, we don't count. However, Objectify only uses 15 | * iterator() and toArray() so this does the job. 16 | * 17 | * Figuring out the actual counts for random-accessing the list would be challenging - 18 | * the underlying GAE List is an async object itself which does not expose its fetching 19 | * behavior. At best we could guess. That can be a future project. 20 | */ 21 | public class InsightList extends InsightIterable implements QueryResultList { 22 | 23 | private final List raw; 24 | 25 | private boolean collected; 26 | 27 | public InsightList(List raw, QueryBatch recorderBatch) { 28 | super(raw, recorderBatch); 29 | 30 | this.raw = raw; 31 | } 32 | 33 | @Override 34 | public List getIndexList() { 35 | return ((QueryResultList)raw).getIndexList(); 36 | } 37 | 38 | @Override 39 | public Cursor getCursor() { 40 | return ((QueryResultList)raw).getCursor(); 41 | } 42 | 43 | @Override 44 | public int size() { 45 | return raw.size(); 46 | } 47 | 48 | @Override 49 | public boolean isEmpty() { 50 | return raw.isEmpty(); 51 | } 52 | 53 | @Override 54 | public boolean contains(Object o) { 55 | return raw.contains(o); 56 | } 57 | 58 | @Override 59 | public Object[] toArray() { 60 | Object[] array = raw.toArray(); 61 | 62 | if (!collected) { 63 | collected = true; 64 | for (Object o : array) 65 | recorderBatch.query((Entity)o); 66 | } 67 | 68 | return array; 69 | } 70 | 71 | @Override 72 | public T[] toArray(T[] a) { 73 | T[] array = raw.toArray(a); 74 | 75 | if (!collected) { 76 | collected = true; 77 | for (Object o : array) 78 | recorderBatch.query((Entity)o); 79 | } 80 | 81 | return array; 82 | } 83 | 84 | @Override 85 | public boolean add(Entity entity) { 86 | return raw.add(entity); 87 | } 88 | 89 | @Override 90 | public boolean remove(Object o) { 91 | return raw.remove(o); 92 | } 93 | 94 | @Override 95 | public boolean containsAll(Collection c) { 96 | return raw.containsAll(c); 97 | } 98 | 99 | @Override 100 | public boolean addAll(Collection c) { 101 | return raw.addAll(c); 102 | } 103 | 104 | @Override 105 | public boolean addAll(int index, Collection c) { 106 | return raw.addAll(index, c); 107 | } 108 | 109 | @Override 110 | public boolean removeAll(Collection c) { 111 | return raw.removeAll(c); 112 | } 113 | 114 | @Override 115 | public boolean retainAll(Collection c) { 116 | return raw.retainAll(c); 117 | } 118 | 119 | @Override 120 | public void clear() { 121 | raw.clear(); 122 | } 123 | 124 | @Override 125 | public Entity get(int index) { 126 | return raw.get(index); 127 | } 128 | 129 | @Override 130 | public Entity set(int index, Entity element) { 131 | return raw.set(index, element); 132 | } 133 | 134 | @Override 135 | public void add(int index, Entity element) { 136 | raw.add(index, element); 137 | } 138 | 139 | @Override 140 | public Entity remove(int index) { 141 | return raw.remove(index); 142 | } 143 | 144 | @Override 145 | public int indexOf(Object o) { 146 | return raw.indexOf(o); 147 | } 148 | 149 | @Override 150 | public int lastIndexOf(Object o) { 151 | return raw.lastIndexOf(o); 152 | } 153 | 154 | @Override 155 | public ListIterator listIterator() { 156 | ListIterator rawIt = raw.listIterator(); 157 | 158 | if (!collected) { 159 | collected = true; 160 | rawIt = InsightIterator.create(rawIt, recorderBatch); 161 | } 162 | 163 | return rawIt; 164 | } 165 | 166 | @Override 167 | public ListIterator listIterator(int index) { 168 | ListIterator rawIt = raw.listIterator(index); 169 | 170 | if (!collected) { 171 | collected = true; 172 | rawIt = InsightIterator.create(rawIt, recorderBatch); 173 | } 174 | 175 | return rawIt; 176 | } 177 | 178 | @Override 179 | public List subList(int fromIndex, int toIndex) { 180 | return raw.subList(fromIndex, toIndex); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/InsightPreparedQuery.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight; 2 | 3 | import com.google.appengine.api.datastore.Entity; 4 | import com.google.appengine.api.datastore.FetchOptions; 5 | import com.google.appengine.api.datastore.PreparedQuery; 6 | import com.google.appengine.api.datastore.QueryResultIterable; 7 | import com.google.appengine.api.datastore.QueryResultIterator; 8 | import com.google.appengine.api.datastore.QueryResultList; 9 | import com.googlecode.objectify.insight.Recorder.QueryBatch; 10 | import lombok.RequiredArgsConstructor; 11 | import java.util.Iterator; 12 | import java.util.List; 13 | 14 | /** 15 | */ 16 | @RequiredArgsConstructor 17 | public class InsightPreparedQuery implements PreparedQuery { 18 | 19 | private final PreparedQuery raw; 20 | 21 | private final QueryBatch recorderBatch; 22 | 23 | @Override 24 | public List asList(FetchOptions fetchOptions) { 25 | return new InsightList(raw.asList(fetchOptions), recorderBatch); 26 | } 27 | 28 | @Override 29 | public QueryResultList asQueryResultList(FetchOptions fetchOptions) { 30 | return new InsightList(raw.asQueryResultList(fetchOptions), recorderBatch); 31 | } 32 | 33 | @Override 34 | public Iterable asIterable(FetchOptions fetchOptions) { 35 | return new InsightIterable(raw.asIterable(fetchOptions), recorderBatch); 36 | } 37 | 38 | @Override 39 | public QueryResultIterable asQueryResultIterable(FetchOptions fetchOptions) { 40 | return new InsightQueryResultIterable(raw.asQueryResultIterable(fetchOptions), recorderBatch); 41 | } 42 | 43 | @Override 44 | public Iterable asIterable() { 45 | return new InsightIterable(raw.asIterable(), recorderBatch); 46 | } 47 | 48 | @Override 49 | public QueryResultIterable asQueryResultIterable() { 50 | return new InsightQueryResultIterable(raw.asQueryResultIterable(), recorderBatch); 51 | } 52 | 53 | @Override 54 | public Iterator asIterator(FetchOptions fetchOptions) { 55 | return InsightIterator.create(raw.asIterator(fetchOptions), recorderBatch); 56 | } 57 | 58 | @Override 59 | public Iterator asIterator() { 60 | return InsightIterator.create(raw.asIterator(), recorderBatch); 61 | } 62 | 63 | @Override 64 | public QueryResultIterator asQueryResultIterator(FetchOptions fetchOptions) { 65 | return InsightIterator.create(raw.asQueryResultIterator(fetchOptions), recorderBatch); 66 | } 67 | 68 | @Override 69 | public QueryResultIterator asQueryResultIterator() { 70 | return InsightIterator.create(raw.asQueryResultIterator(), recorderBatch); 71 | } 72 | 73 | @Override 74 | public Entity asSingleEntity() throws TooManyResultsException { 75 | Entity ent = raw.asSingleEntity(); 76 | recorderBatch.query(ent); 77 | return ent; 78 | } 79 | 80 | @Override 81 | public int countEntities(FetchOptions fetchOptions) { 82 | return raw.countEntities(fetchOptions); 83 | } 84 | 85 | @Override 86 | public int countEntities() { 87 | return raw.countEntities(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/InsightQueryResultIterable.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight; 2 | 3 | import com.google.appengine.api.datastore.Entity; 4 | import com.google.appengine.api.datastore.QueryResultIterable; 5 | import com.google.appengine.api.datastore.QueryResultIterator; 6 | import com.googlecode.objectify.insight.Recorder.QueryBatch; 7 | import lombok.RequiredArgsConstructor; 8 | 9 | /** 10 | */ 11 | @RequiredArgsConstructor 12 | public class InsightQueryResultIterable implements QueryResultIterable { 13 | 14 | private final QueryResultIterable raw; 15 | 16 | protected final QueryBatch recorderBatch; 17 | 18 | private boolean collected; 19 | 20 | @Override 21 | public QueryResultIterator iterator() { 22 | if (collected) { 23 | return raw.iterator(); 24 | } else { 25 | collected = true; 26 | return InsightIterator.create(raw.iterator(), recorderBatch); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/Operation.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight; 2 | 3 | import com.google.api.client.util.Value; 4 | 5 | /** 6 | * Possible operations we track as a dimension 7 | */ 8 | public enum Operation { 9 | @Deprecated 10 | @Value LOAD, // temporarily here for backwards compatibility; will be removed in next version 11 | 12 | @Value GET, 13 | @Value QUERY, 14 | @Value INSERT, 15 | @Value UPDATE, 16 | @Value DELETE 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/Recorder.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight; 2 | 3 | import com.google.appengine.api.NamespaceManager; 4 | import com.google.appengine.api.datastore.Entity; 5 | import com.google.appengine.api.datastore.Key; 6 | import com.google.appengine.api.datastore.Query; 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | import javax.inject.Inject; 10 | import javax.inject.Singleton; 11 | import java.util.HashSet; 12 | import java.util.Set; 13 | 14 | /** 15 | * Make it easier to record activities. By default records only kinds that you 16 | * register with recordKind(), but if you setRecordAll(), it will record everything. 17 | */ 18 | @Singleton 19 | public class Recorder { 20 | 21 | @Getter 22 | private final BucketFactory bucketFactory; 23 | 24 | @Getter 25 | private final Collector collector; 26 | 27 | @Getter 28 | private final Codepointer codepointer; 29 | 30 | private final Set recordKinds = new HashSet<>(); 31 | 32 | @Getter @Setter 33 | private boolean recordAll; 34 | 35 | @Inject 36 | public Recorder(BucketFactory bucketFactory, Collector collector, Codepointer codepointer) { 37 | this.bucketFactory = bucketFactory; 38 | this.collector = collector; 39 | this.codepointer = codepointer; 40 | } 41 | 42 | /** 43 | *

Add a kind to the list of kinds that get recorded. Unless recordAll is set, 44 | * only these kinds will be recorded.

45 | * 46 | *

This method is not thread-safe; register all kinds at application startup, 47 | * before you begin using the InsightAsyncDatastoreService.

48 | */ 49 | public void recordKind(String kind) { 50 | recordKinds.add(kind); 51 | } 52 | 53 | /** */ 54 | private boolean shouldRecord(String kind) { 55 | return recordAll || recordKinds.contains(kind); 56 | } 57 | 58 | /** Create a new batch for recording */ 59 | public Batch batch() { 60 | return new Batch(); 61 | } 62 | 63 | /** Create a new batch for recording query data */ 64 | public QueryBatch query(Query query) { 65 | return new QueryBatch(query); 66 | } 67 | 68 | /** 69 | * A session of recording associated with a particular code point. 70 | */ 71 | public class Batch { 72 | 73 | protected final String codePoint; 74 | 75 | Batch() { 76 | codePoint = codepointer.getCodepoint(); 77 | } 78 | 79 | /** 80 | */ 81 | public void get(Key key) { 82 | if (shouldRecord(key.getKind())) 83 | collector.collect(bucketFactory.forGet(codePoint, key.getNamespace(), key.getKind(), 1)); 84 | } 85 | 86 | /** 87 | */ 88 | public void put(Entity entity) { 89 | if (shouldRecord(entity.getKind())) 90 | collector.collect(bucketFactory.forPut(codePoint, entity.getNamespace(), entity.getKind(), !entity.getKey().isComplete(), 1)); 91 | } 92 | 93 | /** 94 | */ 95 | public void delete(Key key) { 96 | if (shouldRecord(key.getKind())) 97 | collector.collect(bucketFactory.forDelete(codePoint, key.getNamespace(), key.getKind(), 1)); 98 | } 99 | } 100 | 101 | public class QueryBatch extends Batch { 102 | protected final String query; 103 | 104 | /** */ 105 | QueryBatch(Query q) { 106 | query = q.toString(); 107 | } 108 | 109 | /** 110 | */ 111 | public void query(Entity entity) { 112 | if (shouldRecord(entity.getKind())) 113 | collector.collect(bucketFactory.forQuery(codePoint, entity.getNamespace(), entity.getKind(), query, 1)); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/puller/BigUploader.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.puller; 2 | 3 | import com.google.api.services.bigquery.Bigquery; 4 | import com.google.api.services.bigquery.model.TableDataInsertAllRequest; 5 | import com.google.api.services.bigquery.model.TableDataInsertAllRequest.Rows; 6 | import com.google.api.services.bigquery.model.TableDataInsertAllResponse; 7 | import com.google.api.services.bigquery.model.TableRow; 8 | import com.google.common.collect.Lists; 9 | import com.googlecode.objectify.insight.Bucket; 10 | import lombok.extern.java.Log; 11 | import javax.inject.Inject; 12 | import java.io.IOException; 13 | import java.util.ArrayList; 14 | import java.util.Collection; 15 | import java.util.List; 16 | 17 | /** 18 | * Does the work of leasing tasks from the task queue, aggregating again, and pushing 19 | * the result to BigQuery. 20 | */ 21 | @Log 22 | public class BigUploader { 23 | 24 | private final Bigquery bigquery; 25 | private final InsightDataset insightDataset; 26 | private final TablePicker tablePicker; 27 | 28 | @Inject 29 | public BigUploader(Bigquery bigquery, InsightDataset insightDataset, TablePicker tablePicker) { 30 | this.bigquery = bigquery; 31 | this.insightDataset = insightDataset; 32 | this.tablePicker = tablePicker; 33 | } 34 | 35 | public void upload(Collection buckets) { 36 | log.finer("Uploading " + buckets.size() + " buckets to bigquery"); 37 | 38 | // Seriously, Google, you f'd up the naming of 'Rows'. The date handling is atrocious. 39 | // And what's with the whole new JSON layer, including annotations? This library is crap. 40 | 41 | List rows = new ArrayList(); 42 | 43 | for (Bucket bucket : buckets) { 44 | TableRow row = new TableRow(); 45 | row.set("uploaded", System.currentTimeMillis() / 1000f); // unix timestamp 46 | row.set("codepoint", bucket.getKey().getCodepoint()); 47 | row.set("namespace", bucket.getKey().getNamespace()); 48 | row.set("module", bucket.getKey().getModule()); 49 | row.set("version", bucket.getKey().getVersion()); 50 | row.set("kind", bucket.getKey().getKind()); 51 | row.set("op", bucket.getKey().getOp()); 52 | row.set("query", bucket.getKey().getQuery()); 53 | row.set("time", bucket.getKey().getTime() / 1000f); // unix timestamp 54 | row.set("reads", bucket.getReads()); 55 | row.set("writes", bucket.getWrites()); 56 | 57 | TableDataInsertAllRequest.Rows rowWrapper = new TableDataInsertAllRequest.Rows(); 58 | 59 | // As much as we would like to do this there isn't really any kind of stable hash because we 60 | // are constantly aggregating. If we really want this, we will have to stop aggregating at the 61 | // task level (the thing that retries). 62 | //rowWrapper.setInsertId(timestamp); 63 | 64 | rowWrapper.setJson(row); 65 | 66 | rows.add(rowWrapper); 67 | } 68 | 69 | // BQ can handle maximum 10000 rows per request 70 | // https://cloud.google.com/bigquery/quotas#streaming_inserts 71 | // The suggested batch size is 500 but I would like to avoid partitioning the rows if it is possible 72 | // so I don't have to worry about buckets that are partially written to BQ 73 | for (List partition: Lists.partition(rows, 10000)) { 74 | TableDataInsertAllRequest request = new TableDataInsertAllRequest().setRows(partition); 75 | request.setIgnoreUnknownValues(true); 76 | 77 | String tableId = tablePicker.pick(); 78 | 79 | try { 80 | TableDataInsertAllResponse response = bigquery 81 | .tabledata() 82 | .insertAll(insightDataset.projectId(), insightDataset.datasetId(), tableId, request) 83 | .execute(); 84 | 85 | if (response.getInsertErrors() != null && !response.getInsertErrors().isEmpty()) { 86 | throw new RuntimeException("There were errors! " + response.getInsertErrors()); 87 | } 88 | } catch (IOException e) { 89 | throw new RuntimeException(e); 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/puller/BucketAggregator.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.puller; 2 | 3 | import com.googlecode.objectify.insight.Bucket; 4 | import com.googlecode.objectify.insight.BucketKey; 5 | import java.util.Collection; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | /** 11 | * Aggregates buckets together. 12 | */ 13 | public class BucketAggregator { 14 | /** */ 15 | Map bucketMap = new HashMap<>(); 16 | 17 | /** */ 18 | public void aggregate(List buckets) { 19 | for (Bucket bucket : buckets) { 20 | Bucket alreadyHere = bucketMap.get(bucket.getKey()); 21 | 22 | if (alreadyHere == null) { 23 | alreadyHere = new Bucket(bucket.getKey()); 24 | bucketMap.put(bucket.getKey(), alreadyHere); 25 | } 26 | 27 | alreadyHere.merge(bucket); 28 | } 29 | } 30 | 31 | /** */ 32 | public Collection getBuckets() { 33 | return bucketMap.values(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/puller/InsightDataset.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.puller; 2 | 3 | /** 4 | * The "address" of the dataset used to store insight data. If you're using guice, you must 5 | * bind this to a real implementation. 6 | */ 7 | public interface InsightDataset { 8 | String projectId(); 9 | String datasetId(); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/puller/Puller.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.puller; 2 | 3 | import com.google.appengine.api.taskqueue.Queue; 4 | import com.googlecode.objectify.insight.BucketList; 5 | import com.googlecode.objectify.insight.util.QueueHelper; 6 | import com.googlecode.objectify.insight.util.TaskHandleHelper; 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | import lombok.extern.java.Log; 10 | import javax.inject.Inject; 11 | import javax.inject.Named; 12 | import javax.inject.Singleton; 13 | import java.util.List; 14 | import java.util.concurrent.TimeUnit; 15 | import java.util.logging.Level; 16 | 17 | /** 18 | * Does the work of leasing tasks from the task queue, aggregating again, and pushing 19 | * the result to BigQuery. 20 | */ 21 | @Log 22 | @Singleton 23 | public class Puller { 24 | 25 | /** */ 26 | public static final int DEFAULT_BATCH_SIZE = 20; 27 | 28 | /** Something long enough to be safe */ 29 | public static final int DEFAULT_LEASE_DURATION_SECONDS = 60 * 10; 30 | 31 | /** */ 32 | private final QueueHelper queue; 33 | 34 | /** */ 35 | private final BigUploader bigUploader; 36 | 37 | /** Number of tasks to lease and aggregate per write to BQ */ 38 | @Getter @Setter 39 | private int batchSize = DEFAULT_BATCH_SIZE; 40 | 41 | /** How long to maintain task leases; short values risk duplicates */ 42 | @Getter @Setter 43 | private int leaseDurationSeconds = DEFAULT_LEASE_DURATION_SECONDS; 44 | 45 | /** 46 | */ 47 | @Inject 48 | public Puller(@Named("insight") Queue queue, BigUploader bigUploader) { 49 | this.queue = new QueueHelper<>(queue, BucketList.class); 50 | this.bigUploader = bigUploader; 51 | } 52 | 53 | /** 54 | * Repeatedly leases batches of tasks, aggregates them, pushes the result to BQ, and deletes the tasks. 55 | * Continues until there are no more tasks to lease or some sort of error is encountered. This method 56 | * should be called regularly on a cron schedule (say, once per minute). 57 | */ 58 | public void execute() { 59 | log.finest("Pulling"); 60 | 61 | while (true) { 62 | try { 63 | if (processOneBatch() < batchSize) 64 | return; 65 | 66 | } catch (RuntimeException ex) { 67 | log.log(Level.WARNING, "Exception while processing insight data; aborting for now", ex); 68 | return; 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * @return the # of tasks actually processed 75 | */ 76 | private int processOneBatch() { 77 | List> handles = queue.lease(leaseDurationSeconds, TimeUnit.SECONDS, batchSize); 78 | 79 | if (!handles.isEmpty()) { 80 | log.finer("Leased " + handles.size() + " bucketlist tasks"); 81 | 82 | BucketAggregator aggregator = new BucketAggregator(); 83 | 84 | for (TaskHandleHelper handle : handles) { 85 | aggregator.aggregate(handle.getPayload().getBuckets()); 86 | } 87 | 88 | bigUploader.upload(aggregator.getBuckets()); 89 | 90 | queue.delete(handles); 91 | } 92 | 93 | return handles.size(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/puller/TablePicker.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.puller; 2 | 3 | import com.google.api.client.googleapis.json.GoogleJsonResponseException; 4 | import com.google.api.services.bigquery.Bigquery; 5 | import com.google.api.services.bigquery.model.Table; 6 | import com.google.api.services.bigquery.model.TableFieldSchema; 7 | import com.google.api.services.bigquery.model.TableReference; 8 | import com.google.api.services.bigquery.model.TableSchema; 9 | import com.google.common.annotations.VisibleForTesting; 10 | 11 | import lombok.Getter; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.Setter; 14 | import lombok.extern.java.Log; 15 | import javax.inject.Inject; 16 | import java.io.IOException; 17 | import java.text.DateFormat; 18 | import java.text.SimpleDateFormat; 19 | import java.util.ArrayList; 20 | import java.util.Date; 21 | import java.util.List; 22 | 23 | /** 24 | * Manages tables on our behalf. Makes sure we have enough tables to pick from. 25 | */ 26 | @Log 27 | public class TablePicker { 28 | /** Number of days ahead to create tables. */ 29 | private static final int DAYS_AHEAD = 7; 30 | 31 | /** */ 32 | private static final long MILLIS_PER_DAY = 1000 * 60 * 60 * 24; 33 | 34 | private final BigqueryHandler bigqueryHandler; 35 | private final InsightDataset insightDataset; 36 | 37 | /** 38 | * Default format is SimpleDateFormat("'OBJSTATS_'yyyyMMdd") which will produce 39 | * values like OBJSTATS_20150114. If you set the format, be sure to include any 40 | * desired table name prefix. 41 | */ 42 | @Getter @Setter 43 | private DateFormat format = new SimpleDateFormat("'OBJSTATS_'yyyyMMdd"); 44 | 45 | @Inject 46 | public TablePicker(Bigquery bigquery, InsightDataset insightDataset) { 47 | this(new DefaultBigqueryHandler(bigquery), insightDataset); 48 | } 49 | 50 | @VisibleForTesting 51 | TablePicker(BigqueryHandler bigqueryHandler, InsightDataset insightDataset) { 52 | this.bigqueryHandler = bigqueryHandler; 53 | this.insightDataset = insightDataset; 54 | } 55 | 56 | /** */ 57 | public String pick() { 58 | return tableIdFor(new Date()); 59 | } 60 | 61 | /** 62 | * Make sure we have a week's worth of tables. This should be called periodically via cron - more than once a week. 63 | */ 64 | public void ensureEnoughTables() throws IOException { 65 | log.finer("Ensuring sufficient tables for " + DAYS_AHEAD + " days"); 66 | 67 | long now = System.currentTimeMillis(); 68 | 69 | for (int i = 0; i < DAYS_AHEAD; i++) { 70 | String tableId = tableIdFor(new Date(now + MILLIS_PER_DAY * i)); 71 | ensureTable(tableId); 72 | } 73 | } 74 | 75 | /** */ 76 | private String tableIdFor(Date date) { 77 | return format.format(date); 78 | } 79 | 80 | /** */ 81 | private void ensureTable(String tableId) throws IOException { 82 | log.finest("Ensuring table exists: " + tableId); 83 | 84 | TableReference reference = new TableReference(); 85 | reference.setProjectId(insightDataset.projectId()); 86 | reference.setDatasetId(insightDataset.datasetId()); 87 | reference.setTableId(tableId); 88 | 89 | Table table = new Table(); 90 | table.setTableReference(reference); 91 | table.setSchema(schema()); 92 | 93 | if (getTable(tableId) == null) { 94 | bigqueryHandler.tablesInsert(insightDataset, table); 95 | } else { 96 | bigqueryHandler.tablesUpdate(insightDataset, tableId, table); 97 | } 98 | } 99 | 100 | private TableSchema schema() { 101 | List fields = new ArrayList<>(); 102 | 103 | fields.add(tableFieldSchema("uploaded", "TIMESTAMP")); 104 | fields.add(tableFieldSchema("codepoint", "STRING")); 105 | fields.add(tableFieldSchema("namespace", "STRING")); 106 | fields.add(tableFieldSchema("module", "STRING")); 107 | fields.add(tableFieldSchema("version", "STRING")); 108 | fields.add(tableFieldSchema("kind", "STRING")); 109 | fields.add(tableFieldSchema("op", "STRING")); 110 | fields.add(tableFieldSchema("query", "STRING")); 111 | fields.add(tableFieldSchema("time", "TIMESTAMP")); 112 | 113 | fields.add(tableFieldSchema("reads", "INTEGER")); 114 | fields.add(tableFieldSchema("writes", "INTEGER")); 115 | 116 | TableSchema schema = new TableSchema(); 117 | schema.setFields(fields); 118 | 119 | return schema; 120 | } 121 | 122 | private TableFieldSchema tableFieldSchema(String name, String type) { 123 | TableFieldSchema field = new TableFieldSchema(); 124 | field.setName(name); 125 | field.setType(type); 126 | return field; 127 | } 128 | 129 | private Table getTable(String tableId) throws IOException { 130 | try { 131 | return bigqueryHandler.tablesGet(insightDataset, tableId); 132 | } catch (GoogleJsonResponseException e) { 133 | if (e.getStatusCode() == 404) { 134 | log.finest("Table " + tableId + " is missing."); 135 | return null; 136 | } else { 137 | throw e; 138 | } 139 | } 140 | } 141 | 142 | @VisibleForTesting 143 | interface BigqueryHandler { 144 | Table tablesGet(InsightDataset insightDataset, String tableId) throws IOException; 145 | Table tablesInsert(InsightDataset insightDataset, Table table) throws IOException; 146 | Table tablesUpdate(InsightDataset insightDataset, String tableId, Table table) throws IOException; 147 | } 148 | 149 | @RequiredArgsConstructor 150 | private static class DefaultBigqueryHandler implements BigqueryHandler { 151 | 152 | private final Bigquery bigquery; 153 | 154 | @Override 155 | public Table tablesGet(InsightDataset insightDataset, String tableId) throws IOException { 156 | return bigquery.tables().get(insightDataset.projectId(), insightDataset.datasetId(), tableId).execute(); 157 | } 158 | 159 | @Override 160 | public Table tablesInsert(InsightDataset insightDataset, Table table) throws IOException { 161 | return bigquery.tables().insert(insightDataset.projectId(), insightDataset.datasetId(), table).execute(); 162 | } 163 | 164 | @Override 165 | public Table tablesUpdate(InsightDataset insightDataset, String tableId, Table table) throws IOException { 166 | return bigquery.tables().update(insightDataset.projectId(), insightDataset.datasetId(), tableId, table).execute(); 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/servlet/AbstractBigQueryServlet.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.servlet; 2 | 3 | import com.google.api.services.bigquery.Bigquery; 4 | import com.googlecode.objectify.insight.puller.InsightDataset; 5 | import javax.servlet.http.HttpServlet; 6 | 7 | /** 8 | * Base servlet for nonguice servlets that use bigquery. Users must extend this class and implement 9 | * the methods that provide Bigquery information. 10 | */ 11 | abstract public class AbstractBigQueryServlet extends HttpServlet { 12 | 13 | private static final long serialVersionUID = 1; 14 | 15 | /** 16 | * Implement this to provide the projectId and datasetId where we will store data. 17 | */ 18 | abstract protected InsightDataset insightDataset(); 19 | 20 | /** 21 | * Implement this to provide the authenticated bigquery object. 22 | */ 23 | abstract protected Bigquery bigquery(); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/servlet/AbstractPullerServlet.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.servlet; 2 | 3 | import com.google.api.services.bigquery.Bigquery; 4 | import com.google.appengine.api.taskqueue.Queue; 5 | import com.google.appengine.api.taskqueue.QueueFactory; 6 | import com.googlecode.objectify.insight.Flusher; 7 | import com.googlecode.objectify.insight.puller.BigUploader; 8 | import com.googlecode.objectify.insight.puller.InsightDataset; 9 | import com.googlecode.objectify.insight.puller.Puller; 10 | import com.googlecode.objectify.insight.puller.TablePicker; 11 | import javax.servlet.ServletException; 12 | import javax.servlet.http.HttpServletRequest; 13 | import javax.servlet.http.HttpServletResponse; 14 | import java.io.IOException; 15 | 16 | /** 17 | * Call this servlet from cron once per minute. It will empty the pull queue and then go back to sleep. 18 | * Tasks are pulled off the pull queue, aggregated, and then pushed to BigQuery. 19 | * 20 | * Extend this if you do not use guice. 21 | */ 22 | abstract public class AbstractPullerServlet extends AbstractBigQueryServlet { 23 | 24 | private static final long serialVersionUID = 1; 25 | 26 | /** */ 27 | private Puller puller; 28 | 29 | /** This is what guice is for */ 30 | @Override 31 | public void init() throws ServletException { 32 | InsightDataset insightDataset = insightDataset(); 33 | Queue queue = QueueFactory.getQueue(queueName()); 34 | Bigquery bigquery = bigquery(); 35 | TablePicker tablePicker = new TablePicker(bigquery, insightDataset); 36 | BigUploader bigUploader = new BigUploader(bigquery, insightDataset, tablePicker); 37 | 38 | puller = new Puller(queue, bigUploader); 39 | } 40 | 41 | /** 42 | * Override this to change the name of the queue we pull from. Be sure to change the value in the Flusher as well. 43 | */ 44 | protected String queueName() { 45 | return Flusher.DEFAULT_QUEUE; 46 | } 47 | 48 | @Override 49 | protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 50 | puller.execute(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/servlet/AbstractTableMakerServlet.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.servlet; 2 | 3 | import com.googlecode.objectify.insight.puller.TablePicker; 4 | import javax.servlet.ServletException; 5 | import javax.servlet.http.HttpServletRequest; 6 | import javax.servlet.http.HttpServletResponse; 7 | import java.io.IOException; 8 | 9 | /** 10 | * Call this servlet from cron once per day. It will make sure there are an appropriate set of tables in bigquery. 11 | * Make sure this servlet starts before the PullerServlet. 12 | * 13 | * Extend this if you do not use guice. 14 | */ 15 | abstract public class AbstractTableMakerServlet extends AbstractBigQueryServlet { 16 | 17 | private static final long serialVersionUID = 1; 18 | 19 | /** */ 20 | private TablePicker tablePicker; 21 | 22 | /** Sets up the picker based on the abstract methods */ 23 | @Override 24 | public void init() throws ServletException { 25 | tablePicker = new TablePicker(bigquery(), insightDataset()); 26 | } 27 | 28 | @Override 29 | protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 30 | tablePicker.ensureEnoughTables(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/servlet/GuicePullerServlet.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.servlet; 2 | 3 | import com.googlecode.objectify.insight.puller.Puller; 4 | import javax.inject.Inject; 5 | import javax.inject.Singleton; 6 | import javax.servlet.ServletException; 7 | import javax.servlet.http.HttpServlet; 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | import java.io.IOException; 11 | 12 | /** 13 | * Call this servlet from cron once per minute. It will empty the pull queue and then go back to sleep. 14 | * Tasks are pulled off the pull queue, aggregated, and then pushed to BigQuery. 15 | */ 16 | @Singleton 17 | public class GuicePullerServlet extends HttpServlet { 18 | 19 | private static final long serialVersionUID = 1; 20 | 21 | /** */ 22 | private final Puller puller; 23 | 24 | /** */ 25 | @Inject 26 | public GuicePullerServlet(Puller puller) { 27 | this.puller = puller; 28 | } 29 | 30 | @Override 31 | protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 32 | puller.execute(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/servlet/GuiceTableMakerServlet.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.servlet; 2 | 3 | import com.googlecode.objectify.insight.puller.TablePicker; 4 | import javax.inject.Inject; 5 | import javax.inject.Singleton; 6 | import javax.servlet.ServletException; 7 | import javax.servlet.http.HttpServlet; 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | import java.io.IOException; 11 | 12 | /** 13 | * Call this servlet from cron once per day. It will make sure there are an appropriate set of tables in bigquery. 14 | * Make sure this servlet starts before the PullerServlet. 15 | */ 16 | @Singleton 17 | public class GuiceTableMakerServlet extends HttpServlet { 18 | 19 | private static final long serialVersionUID = 1; 20 | 21 | /** */ 22 | private final TablePicker tablePicker; 23 | 24 | /** */ 25 | @Inject 26 | public GuiceTableMakerServlet(TablePicker tablePicker) { 27 | this.tablePicker = tablePicker; 28 | } 29 | 30 | @Override 31 | protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 32 | tablePicker.ensureEnoughTables(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/util/QueueHelper.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.util; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.google.appengine.api.taskqueue.Queue; 6 | import com.google.appengine.api.taskqueue.QueueConstants; 7 | import com.google.appengine.api.taskqueue.TaskHandle; 8 | import com.google.appengine.api.taskqueue.TaskOptions; 9 | import com.google.common.base.Function; 10 | import com.google.common.collect.Iterables; 11 | import com.google.common.collect.Lists; 12 | import lombok.Data; 13 | import java.util.List; 14 | import java.util.concurrent.TimeUnit; 15 | 16 | /** Just a slightly more convenient interface for our purposes */ 17 | @Data 18 | public class QueueHelper { 19 | static final ObjectMapper MAPPER = new ObjectMapper(); 20 | 21 | /** */ 22 | private final Queue queue; 23 | private final Class clazz; 24 | 25 | /** */ 26 | public void add(T jsonifyMe) { 27 | queue.addAsync(null, makeTask(jsonifyMe)); 28 | } 29 | 30 | /** Allows any number of tasks; automatically partitions as necessary */ 31 | public void add(Iterable payloads) { 32 | Iterable opts = Iterables.transform(payloads, new Function() { 33 | @Override 34 | public TaskOptions apply(T thing) { 35 | return makeTask(thing); 36 | } 37 | }); 38 | 39 | Iterable> partitioned = Iterables.partition(opts, QueueConstants.maxTasksPerAdd()); 40 | 41 | for (List piece: partitioned) 42 | queue.addAsync(null, piece); 43 | } 44 | 45 | /** */ 46 | private TaskOptions makeTask(T jsonifyMe) { 47 | try { 48 | byte[] payload = MAPPER.writeValueAsBytes(jsonifyMe); 49 | return TaskOptions.Builder.withMethod(TaskOptions.Method.PULL).payload(payload); 50 | } catch (JsonProcessingException e) { 51 | throw new RuntimeException(e); 52 | } 53 | } 54 | 55 | /** */ 56 | public List> lease(int duration, TimeUnit units, int count) { 57 | List handles = queue.leaseTasks(duration, units, count); 58 | 59 | return Lists.transform(handles, new Function>() { 60 | @Override 61 | public TaskHandleHelper apply(TaskHandle taskHandle) { 62 | return new TaskHandleHelper(taskHandle, clazz); 63 | } 64 | }); 65 | } 66 | 67 | /** */ 68 | public void delete(List> handles) { 69 | List raw = Lists.transform(handles, new Function, TaskHandle>() { 70 | @Override 71 | public TaskHandle apply(TaskHandleHelper input) { 72 | return input.getRaw(); 73 | } 74 | }); 75 | 76 | queue.deleteTaskAsync(raw); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/util/ReflectionUtils.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.util; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | /** 6 | * Working around more checked exception brain damage 7 | */ 8 | public class ReflectionUtils { 9 | public static Method getMethod(Class clazz, String methodName, Class... parameterTypes) { 10 | try { 11 | return clazz.getMethod(methodName, parameterTypes); 12 | } catch (NoSuchMethodException e) { 13 | throw new RuntimeException(e); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/util/StackTraceUtils.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.util; 2 | 3 | import java.util.regex.Matcher; 4 | import java.util.regex.Pattern; 5 | 6 | public class StackTraceUtils { 7 | 8 | private static final String ENHANCER_BY = "EnhancerBy"; 9 | private static final String FAST_CLASS_BY = "FastClassBy"; 10 | 11 | // http://regex101.com/r/aC1pS0/5 12 | private static final Pattern pattern = Pattern.compile("((?:" + ENHANCER_BY + "|" + FAST_CLASS_BY + ")\\w+)(\\${2})(\\w+)(\\${0,2}\\.?)"); 13 | 14 | public static String removeMutableEnhancements(String oldStack) { 15 | // much faster than pattern.matcher 16 | if (!containsMutableEnhancements(oldStack)) { 17 | return oldStack; 18 | } 19 | 20 | String stack = oldStack; 21 | Matcher m = pattern.matcher(stack); 22 | if (m.find()) { 23 | // replace first number with "number" and second number with the first 24 | stack = m.replaceAll("$1$2$4"); 25 | } 26 | return stack; 27 | } 28 | 29 | public static boolean containsMutableEnhancements(String s) { 30 | return s.contains(ENHANCER_BY) || s.contains(FAST_CLASS_BY); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/googlecode/objectify/insight/util/TaskHandleHelper.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.util; 2 | 3 | import com.google.appengine.api.taskqueue.TaskHandle; 4 | import lombok.RequiredArgsConstructor; 5 | import java.io.IOException; 6 | 7 | /** */ 8 | @RequiredArgsConstructor 9 | public class TaskHandleHelper { 10 | private final TaskHandle handle; 11 | private final Class clazz; 12 | 13 | /** */ 14 | public T getPayload() { 15 | try { 16 | return QueueHelper.MAPPER.readValue(handle.getPayload(), clazz); 17 | } catch (IOException e) { 18 | throw new RuntimeException(e); 19 | } 20 | } 21 | 22 | /** */ 23 | public TaskHandle getRaw() { 24 | return handle; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/com/googlecode/objectify/insight/CodepointerTest.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight; 2 | 3 | import static org.hamcrest.Matchers.containsString; 4 | import static org.hamcrest.Matchers.equalTo; 5 | import static org.hamcrest.Matchers.greaterThan; 6 | import static org.hamcrest.Matchers.not; 7 | import static org.junit.Assert.assertThat; 8 | 9 | import org.hamcrest.Matchers; 10 | import org.testng.annotations.Test; 11 | 12 | import com.google.common.collect.Iterables; 13 | import com.google.common.collect.Lists; 14 | import com.googlecode.objectify.insight.Codepointer.AdvancedStackProducer; 15 | import com.googlecode.objectify.insight.Codepointer.LegacyStackProducer; 16 | 17 | /** 18 | */ 19 | public class CodepointerTest { 20 | 21 | @Test 22 | public void codepointingCanBeDisabled() throws Exception { 23 | Codepointer codepointer = new Codepointer(); 24 | assertThat(codepointer.getCodepoint(), not(equalTo("disabled"))); 25 | codepointer.setDisabled(true); 26 | assertThat(codepointer.getCodepoint(), equalTo("disabled")); 27 | } 28 | 29 | @Test 30 | public void testDefaultStackProducer() { 31 | assertThat(new Codepointer().getStackProducer(), Matchers.instanceOf(LegacyStackProducer.class)); 32 | } 33 | 34 | @Test 35 | public void testCodepointForCompacterStack() { 36 | final StackTraceElement ste = new StackTraceElement("className", "methodName", "fileName", 1); 37 | final String compactedStack = Exception.class.getName() + "\r\n" + "\tat " + ste.toString() + "\r\n"; 38 | 39 | final String codepoint1 = new Codepointer().setStackProducer(new LegacyStackProducer()).getCodepoint(); 40 | final String codepoint2 = new Codepointer().setStackProducer(new AdvancedStackProducer()).getCodepoint(); 41 | final String codepoint3 = new Codepointer().setStackProducer(new AdvancedStackProducer() { 42 | @Override 43 | protected Iterable filterStack(Iterable stack) { 44 | return stack; 45 | } 46 | }).getCodepoint(); 47 | final String codepoint4 = new Codepointer().setStackProducer(new AdvancedStackProducer() { 48 | @Override 49 | protected Iterable filterStack(Iterable stack) { 50 | return Lists.newArrayList(ste); 51 | } 52 | }).getCodepoint(); 53 | 54 | assertThat(codepoint3, not(equalTo(codepoint2))); 55 | 56 | assertThat(codepoint4, not(equalTo(codepoint1))); 57 | assertThat(codepoint4, not(equalTo(codepoint2))); 58 | assertThat(codepoint4, not(equalTo(codepoint3))); 59 | assertThat(codepoint4, equalTo(new Codepointer().digest(compactedStack))); 60 | } 61 | 62 | @Test 63 | public void testCompacterStack() { 64 | final String stack1 = new Codepointer().setStackProducer(new LegacyStackProducer()).stack(); 65 | final String stack2 = new Codepointer().setStackProducer(new AdvancedStackProducer()).stack(); 66 | final String stack3 = new Codepointer().setStackProducer(new AdvancedStackProducer() { 67 | @Override 68 | protected Iterable filterStack(Iterable stack) { 69 | return stack; 70 | } 71 | }).stack(); 72 | final String stack4 = new Codepointer().setStackProducer(new AdvancedStackProducer() { 73 | @Override 74 | protected Iterable filterStack(Iterable stack) { 75 | return Iterables.limit(stack, 1); 76 | } 77 | }).stack(); 78 | 79 | assertThat(getStackTraceElementCount(stack1), greaterThan(2)); 80 | assertThat(getStackTraceElementCount(stack2), greaterThan(2)); 81 | assertThat(getStackTraceElementCount(stack3), greaterThan(2)); 82 | assertThat(getStackTraceElementCount(stack4), equalTo(1)); 83 | } 84 | 85 | private int getStackTraceElementCount(final String stack) { 86 | return stack.split("\tat").length - 1; 87 | } 88 | 89 | @Test 90 | public void testAdvancedFiltering() { 91 | final String stack1 = new Codepointer().setStackProducer(new LegacyStackProducer()).stack(); 92 | final String stack2 = new Codepointer().setStackProducer(new AdvancedStackProducer()).stack(); 93 | 94 | assertThat(stack1, containsString("com.googlecode.objectify.")); 95 | assertThat(stack2, not(containsString("com.googlecode.objectify."))); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/test/java/com/googlecode/objectify/insight/puller/TablePickerTest.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.puller; 2 | 3 | import com.google.api.client.googleapis.json.GoogleJsonResponseException; 4 | import com.google.api.client.googleapis.testing.json.GoogleJsonResponseExceptionFactoryTesting; 5 | import com.google.api.client.testing.json.MockJsonFactory; 6 | import com.google.api.services.bigquery.model.Table; 7 | import com.googlecode.objectify.insight.puller.InsightDataset; 8 | import com.googlecode.objectify.insight.puller.TablePicker; 9 | import com.googlecode.objectify.insight.puller.TablePicker.BigqueryHandler; 10 | import com.googlecode.objectify.insight.test.util.TestBase; 11 | import org.mockito.Mock; 12 | import org.mockito.stubbing.OngoingStubbing; 13 | import org.testng.annotations.BeforeMethod; 14 | import org.testng.annotations.Test; 15 | 16 | import java.io.IOException; 17 | import java.text.SimpleDateFormat; 18 | import java.util.Date; 19 | import static org.hamcrest.Matchers.equalTo; 20 | import static org.junit.Assert.assertThat; 21 | import static org.mockito.Matchers.any; 22 | import static org.mockito.Mockito.when; 23 | 24 | /** 25 | */ 26 | public class TablePickerTest extends TestBase { 27 | 28 | @Mock BigqueryHandler bigqueryHandler; 29 | @Mock InsightDataset insightDataset; 30 | 31 | private TablePicker picker; 32 | 33 | @BeforeMethod 34 | public void before() { 35 | when(insightDataset.projectId()).thenReturn("foo"); 36 | when(insightDataset.datasetId()).thenReturn("bar"); 37 | 38 | picker = new TablePicker(bigqueryHandler, insightDataset); 39 | } 40 | 41 | @Test 42 | public void pickerPicksTheRightName() throws Exception { 43 | String picked = picker.pick(); 44 | 45 | String expected = "OBJSTATS_" + new SimpleDateFormat("yyyyMMdd").format(new Date()); 46 | 47 | assertThat(picked, equalTo(expected)); 48 | } 49 | 50 | @Test 51 | public void ensuringTablesWorksWithoutExistingTables() throws Exception { 52 | whenTablesGet().thenThrow(notFoundGoogleJsonResponseException()); 53 | whenTablesInsert().thenReturn(new Table()); 54 | whenTablesUpdate().thenThrow(notFoundGoogleJsonResponseException()); 55 | 56 | // It's enough just to make sure this doesn't produce a higher level exception 57 | picker.ensureEnoughTables(); 58 | } 59 | 60 | @Test 61 | public void ensuringTablesWorksEvenIfTablesExist() throws Exception { 62 | whenTablesGet().thenReturn(new Table()); 63 | whenTablesInsert().thenThrow(duplicateGoogleJsonResponseException()); 64 | whenTablesUpdate().thenReturn(new Table()); 65 | 66 | // It's enough just to make sure this doesn't produce a higher level exception 67 | picker.ensureEnoughTables(); 68 | } 69 | 70 | @Test(expectedExceptions = GoogleJsonResponseException.class) 71 | public void ensuringTablesFailsIfNonExistingTablesCantBeInserted() throws Exception { 72 | whenTablesGet().thenThrow(notFoundGoogleJsonResponseException()); 73 | whenTablesInsert().thenThrow(unknownGoogleJsonResponseException()); 74 | 75 | // It's enough just to make sure this doesn't produce a higher level exception 76 | picker.ensureEnoughTables(); 77 | } 78 | 79 | @Test(expectedExceptions = GoogleJsonResponseException.class) 80 | public void ensuringTablesFailsIfExistingTablesCantBeUpdated() throws Exception { 81 | whenTablesGet().thenReturn(new Table()); 82 | whenTablesUpdate().thenThrow(unknownGoogleJsonResponseException()); 83 | 84 | // It's enough just to make sure this doesn't produce a higher level exception 85 | picker.ensureEnoughTables(); 86 | } 87 | 88 | private OngoingStubbing whenTablesGet() throws IOException { 89 | return when(bigqueryHandler.tablesGet(any(InsightDataset.class), any(String.class))); 90 | } 91 | 92 | private OngoingStubbing
whenTablesInsert() throws IOException { 93 | return when(bigqueryHandler.tablesInsert(any(InsightDataset.class), any(Table.class))); 94 | } 95 | 96 | private OngoingStubbing
whenTablesUpdate() throws IOException { 97 | return when(bigqueryHandler.tablesUpdate(any(InsightDataset.class), any(String.class), any(Table.class))); 98 | } 99 | 100 | private GoogleJsonResponseException notFoundGoogleJsonResponseException() throws IOException { 101 | return googleJsonResponseException(404, "notFound"); 102 | } 103 | 104 | private GoogleJsonResponseException duplicateGoogleJsonResponseException() throws IOException { 105 | return googleJsonResponseException(409, "duplicate"); 106 | } 107 | 108 | private GoogleJsonResponseException unknownGoogleJsonResponseException() throws IOException { 109 | return googleJsonResponseException(500, "unknown"); 110 | } 111 | 112 | private GoogleJsonResponseException googleJsonResponseException(int httpCode, String reasonPhrase) throws IOException { 113 | return GoogleJsonResponseExceptionFactoryTesting.newMock(new MockJsonFactory(), httpCode, reasonPhrase); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/test/java/com/googlecode/objectify/insight/puller/test/PullerTest.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.puller.test; 2 | 3 | import com.google.appengine.api.taskqueue.Queue; 4 | import com.google.appengine.api.taskqueue.TaskHandle; 5 | import com.googlecode.objectify.insight.Bucket; 6 | import com.googlecode.objectify.insight.BucketFactory; 7 | import com.googlecode.objectify.insight.BucketList; 8 | import com.googlecode.objectify.insight.puller.BigUploader; 9 | import com.googlecode.objectify.insight.puller.Puller; 10 | import com.googlecode.objectify.insight.test.util.TestBase; 11 | import org.mockito.Mock; 12 | import org.testng.annotations.BeforeMethod; 13 | import org.testng.annotations.Test; 14 | import java.util.Arrays; 15 | import java.util.concurrent.TimeUnit; 16 | import static org.mockito.Mockito.verify; 17 | import static org.mockito.Mockito.when; 18 | 19 | /** 20 | */ 21 | public class PullerTest extends TestBase { 22 | 23 | @Mock private Queue queue; 24 | @Mock private BigUploader bigUploader; 25 | 26 | private BucketFactory bucketFactory; 27 | private Bucket bucket1; 28 | private Bucket bucket2; 29 | private Bucket bucket3; 30 | private Bucket bucket4; 31 | private BucketList bucketList1; 32 | private BucketList bucketList2; 33 | private Puller puller; 34 | 35 | @BeforeMethod 36 | public void setUpFixture() throws Exception { 37 | bucketFactory = new BucketFactory(); 38 | 39 | bucket1 = bucketFactory.forGet("here", "ns", "kindA", 11); 40 | bucket2 = bucketFactory.forGet("here", "ns", "kindB", 22); 41 | bucket3 = bucketFactory.forGet("here", "ns", "kindA", 33); 42 | bucket4 = bucketFactory.forGet("here", "ns", "kindB", 44); 43 | 44 | bucketList1 = new BucketList(Arrays.asList(bucket1, bucket2)); 45 | bucketList2 = new BucketList(Arrays.asList(bucket3, bucket4)); 46 | 47 | puller = new Puller(queue, bigUploader); 48 | } 49 | 50 | @Test 51 | public void uploadsAggregatedLeasedTasks() throws Exception { 52 | 53 | TaskHandle taskHandle1 = makeTaskHandle(bucketList1); 54 | TaskHandle taskHandle2 = makeTaskHandle(bucketList2); 55 | 56 | when(queue.leaseTasks(Puller.DEFAULT_LEASE_DURATION_SECONDS, TimeUnit.SECONDS, Puller.DEFAULT_BATCH_SIZE)) 57 | .thenReturn(Arrays.asList(taskHandle1, taskHandle2)); 58 | 59 | puller.execute(); 60 | 61 | verify(queue).deleteTaskAsync(Arrays.asList(taskHandle1, taskHandle2)); 62 | 63 | verify(bigUploader).upload(buckets(Arrays.asList( 64 | bucketFactory.forGet("here", "ns", "kindA", 44), 65 | bucketFactory.forGet("here", "ns", "kindB", 66) 66 | ))); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/com/googlecode/objectify/insight/test/FlusherTest.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.test; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.google.appengine.api.datastore.Transaction; 5 | import com.google.appengine.api.taskqueue.Queue; 6 | import com.google.appengine.api.taskqueue.TaskOptions; 7 | import com.googlecode.objectify.insight.Bucket; 8 | import com.googlecode.objectify.insight.BucketFactory; 9 | import com.googlecode.objectify.insight.BucketList; 10 | import com.googlecode.objectify.insight.Flusher; 11 | import com.googlecode.objectify.insight.test.util.TestBase; 12 | import org.mockito.ArgumentCaptor; 13 | import org.mockito.Captor; 14 | import org.mockito.Mock; 15 | import org.testng.annotations.BeforeMethod; 16 | import org.testng.annotations.Test; 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | import static org.hamcrest.CoreMatchers.equalTo; 20 | import static org.junit.Assert.assertThat; 21 | import static org.mockito.Matchers.isNull; 22 | import static org.mockito.Mockito.verify; 23 | 24 | /** 25 | */ 26 | public class FlusherTest extends TestBase { 27 | 28 | @Mock private Queue queue; 29 | @Captor private ArgumentCaptor taskCaptor; 30 | 31 | private BucketFactory bucketFactory; 32 | private Flusher flusher; 33 | 34 | @BeforeMethod 35 | public void setUpFixture() throws Exception { 36 | bucketFactory = constantTimeBucketFactory(); 37 | flusher = new Flusher(queue); 38 | } 39 | 40 | @Test 41 | public void flushingBucketsProducesJsonPullTask() throws Exception { 42 | List buckets = new ArrayList<>(); 43 | buckets.add(bucketFactory.forGet("here", "ns", "kindA", 123)); 44 | buckets.add(bucketFactory.forGet("here", "ns", "kindB", 456)); 45 | 46 | flusher.flush(buckets); 47 | 48 | verify(queue).addAsync(isNull(Transaction.class), taskCaptor.capture()); 49 | TaskOptions parameter = taskCaptor.getValue(); 50 | 51 | byte[] expectedJson = new ObjectMapper().writeValueAsBytes(new BucketList(buckets)); 52 | 53 | assertThat(parameter.getPayload(), equalTo(expectedJson)); 54 | 55 | // damn, no way to check headers for content type because there is no accessor on TaskOptions 56 | } 57 | 58 | } 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/test/java/com/googlecode/objectify/insight/test/InsightAsyncDatastoreServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.test; 2 | 3 | import com.google.appengine.api.datastore.AsyncDatastoreService; 4 | import com.google.appengine.api.datastore.Entity; 5 | import com.google.appengine.api.datastore.Key; 6 | import com.google.appengine.api.datastore.KeyFactory; 7 | import com.google.appengine.api.datastore.PreparedQuery; 8 | import com.google.appengine.api.datastore.Query; 9 | import com.google.appengine.api.datastore.Transaction; 10 | import com.googlecode.objectify.insight.BucketFactory; 11 | import com.googlecode.objectify.insight.Codepointer; 12 | import com.googlecode.objectify.insight.Collector; 13 | import com.googlecode.objectify.insight.InsightAsyncDatastoreService; 14 | import com.googlecode.objectify.insight.InsightPreparedQuery; 15 | import com.googlecode.objectify.insight.Recorder; 16 | import com.googlecode.objectify.insight.test.util.TestBase; 17 | import org.mockito.Mock; 18 | import org.testng.annotations.BeforeMethod; 19 | import org.testng.annotations.Test; 20 | import java.util.Arrays; 21 | import java.util.Collections; 22 | import static org.hamcrest.Matchers.instanceOf; 23 | import static org.hamcrest.Matchers.not; 24 | import static org.junit.Assert.assertThat; 25 | import static org.mockito.Mockito.verify; 26 | import static org.mockito.Mockito.when; 27 | 28 | /** 29 | * Tests the nonquery methods. Query methods are tested in InsightPreparedQueryTest. 30 | */ 31 | public class InsightAsyncDatastoreServiceTest extends TestBase { 32 | 33 | private InsightAsyncDatastoreService service; 34 | private BucketFactory bucketFactory; 35 | 36 | @Mock private AsyncDatastoreService raw; 37 | @Mock private Collector collector; 38 | @Mock private Codepointer codepointer; 39 | 40 | @BeforeMethod 41 | public void setUpFixture() throws Exception { 42 | bucketFactory = constantTimeBucketFactory(); 43 | 44 | when(codepointer.getCodepoint()).thenReturn("here"); 45 | 46 | Recorder recorder = new Recorder(bucketFactory, collector, codepointer); 47 | recorder.setRecordAll(true); 48 | 49 | service = new InsightAsyncDatastoreService(raw, recorder); 50 | } 51 | 52 | @Test 53 | public void getIsCollected() throws Exception { 54 | runInNamespace("ns", new Runnable() { 55 | @Override 56 | public void run() { 57 | Key key = KeyFactory.createKey("Thing", 123L); 58 | service.get(key); 59 | } 60 | }); 61 | 62 | verify(collector).collect(bucketFactory.forGet("here", "ns", "Thing", 1)); 63 | } 64 | 65 | @Test 66 | public void getWithTxnIsCollected() throws Exception { 67 | runInNamespace("ns", new Runnable() { 68 | @Override 69 | public void run() { 70 | Key key = KeyFactory.createKey("Thing", 123L); 71 | service.get(null, key); 72 | } 73 | }); 74 | 75 | verify(collector).collect(bucketFactory.forGet("here", "ns", "Thing", 1)); 76 | } 77 | 78 | @Test 79 | public void getMultiIsCollected() throws Exception { 80 | runInNamespace("ns", new Runnable() { 81 | @Override 82 | public void run() { 83 | Key key = KeyFactory.createKey("Thing", 123L); 84 | service.get(Collections.singleton(key)); 85 | } 86 | }); 87 | 88 | verify(collector).collect(bucketFactory.forGet("here", "ns", "Thing", 1)); 89 | } 90 | 91 | @Test 92 | public void getMultiWithTxnIsCollected() throws Exception { 93 | runInNamespace("ns", new Runnable() { 94 | @Override 95 | public void run() { 96 | Key key = KeyFactory.createKey("Thing", 123L); 97 | service.get(null, Collections.singleton(key)); 98 | } 99 | }); 100 | 101 | verify(collector).collect(bucketFactory.forGet("here", "ns", "Thing", 1)); 102 | } 103 | 104 | @Test 105 | public void putInsertIsCollected() throws Exception { 106 | runInNamespace("ns", new Runnable() { 107 | @Override 108 | public void run() { 109 | Entity ent = new Entity("Thing"); // no id 110 | service.put(ent); 111 | } 112 | }); 113 | 114 | verify(collector).collect(bucketFactory.forPut("here", "ns", "Thing", true, 1)); 115 | } 116 | 117 | @Test 118 | public void putUpdateIsCollected() throws Exception { 119 | runInNamespace("ns", new Runnable() { 120 | @Override 121 | public void run() { 122 | Entity ent = new Entity("Thing", 123L); 123 | service.put(ent); 124 | } 125 | }); 126 | 127 | verify(collector).collect(bucketFactory.forPut("here", "ns", "Thing", false, 1)); 128 | } 129 | 130 | @Test 131 | public void putWithTxnInsertIsCollected() throws Exception { 132 | runInNamespace("ns", new Runnable() { 133 | @Override 134 | public void run() { 135 | Entity ent = new Entity("Thing"); // no id 136 | service.put(null, ent); 137 | } 138 | }); 139 | 140 | verify(collector).collect(bucketFactory.forPut("here", "ns", "Thing", true, 1)); 141 | } 142 | 143 | @Test 144 | public void putWithTxnUpdateIsCollected() throws Exception { 145 | runInNamespace("ns", new Runnable() { 146 | @Override 147 | public void run() { 148 | Entity ent = new Entity("Thing", 123L); 149 | service.put(null, ent); 150 | } 151 | }); 152 | 153 | verify(collector).collect(bucketFactory.forPut("here", "ns", "Thing", false, 1)); 154 | } 155 | 156 | @Test 157 | public void putMultiIsCollected() throws Exception { 158 | runInNamespace("ns", new Runnable() { 159 | @Override 160 | public void run() { 161 | service.put(Arrays.asList(new Entity("Thing"), new Entity("Thing", 123L))); 162 | } 163 | }); 164 | 165 | verify(collector).collect(bucketFactory.forPut("here", "ns", "Thing", true, 1)); 166 | verify(collector).collect(bucketFactory.forPut("here", "ns", "Thing", false, 1)); 167 | } 168 | 169 | @Test 170 | public void putMultiWithTxnIsCollected() throws Exception { 171 | runInNamespace("ns", new Runnable() { 172 | @Override 173 | public void run() { 174 | service.put(null, Arrays.asList(new Entity("Thing"), new Entity("Thing", 123L))); 175 | } 176 | }); 177 | 178 | verify(collector).collect(bucketFactory.forPut("here", "ns", "Thing", true, 1)); 179 | verify(collector).collect(bucketFactory.forPut("here", "ns", "Thing", false, 1)); 180 | } 181 | 182 | @Test 183 | public void deleteIsCollected() throws Exception { 184 | runInNamespace("ns", new Runnable() { 185 | @Override 186 | public void run() { 187 | Key key = KeyFactory.createKey("Thing", 123L); 188 | service.delete(key); 189 | } 190 | }); 191 | 192 | verify(collector).collect(bucketFactory.forDelete("here", "ns", "Thing", 1)); 193 | } 194 | 195 | @Test 196 | public void deleteWithTxnIsCollected() throws Exception { 197 | runInNamespace("ns", new Runnable() { 198 | @Override 199 | public void run() { 200 | Key key = KeyFactory.createKey("Thing", 123L); 201 | service.delete((Transaction)null, key); 202 | } 203 | }); 204 | 205 | verify(collector).collect(bucketFactory.forDelete("here", "ns", "Thing", 1)); 206 | } 207 | 208 | @Test 209 | public void deleteMultiIsCollected() throws Exception { 210 | runInNamespace("ns", new Runnable() { 211 | @Override 212 | public void run() { 213 | Key key = KeyFactory.createKey("Thing", 123L); 214 | service.delete(Collections.singleton(key)); 215 | } 216 | }); 217 | 218 | verify(collector).collect(bucketFactory.forDelete("here", "ns", "Thing", 1)); 219 | } 220 | 221 | @Test 222 | public void deleteMultiWithTxnIsCollected() throws Exception { 223 | runInNamespace("ns", new Runnable() { 224 | @Override 225 | public void run() { 226 | Key key = KeyFactory.createKey("Thing", 123L); 227 | service.delete(null, Collections.singleton(key)); 228 | } 229 | }); 230 | 231 | verify(collector).collect(bucketFactory.forDelete("here", "ns", "Thing", 1)); 232 | } 233 | 234 | @Test 235 | public void keysOnlyQueriesAreNotCollected() throws Exception { 236 | Query query = new Query(); 237 | query.setKeysOnly(); 238 | 239 | PreparedQuery pq = service.prepare(query); 240 | 241 | assertThat(pq, not(instanceOf(InsightPreparedQuery.class))); 242 | } 243 | 244 | @Test 245 | public void keysOnlyTxnQueriesAreNotCollected() throws Exception { 246 | Query query = new Query(); 247 | query.setKeysOnly(); 248 | 249 | PreparedQuery pq = service.prepare(null, query); 250 | 251 | assertThat(pq, not(instanceOf(InsightPreparedQuery.class))); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/test/java/com/googlecode/objectify/insight/test/InsightCollectorTest.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.test; 2 | 3 | import com.googlecode.objectify.insight.Bucket; 4 | import com.googlecode.objectify.insight.BucketFactory; 5 | import com.googlecode.objectify.insight.Collector; 6 | import com.googlecode.objectify.insight.Flusher; 7 | import com.googlecode.objectify.insight.test.util.TestBase; 8 | import org.mockito.Mock; 9 | import org.testng.annotations.BeforeMethod; 10 | import org.testng.annotations.Test; 11 | import java.util.Iterator; 12 | import java.util.LinkedHashSet; 13 | import java.util.Set; 14 | import static org.mockito.Matchers.anyCollectionOf; 15 | import static org.mockito.Mockito.never; 16 | import static org.mockito.Mockito.verify; 17 | 18 | /** 19 | */ 20 | public class InsightCollectorTest extends TestBase { 21 | 22 | @Mock private Flusher flusher; 23 | 24 | private Collector collector; 25 | private BucketFactory bucketFactory; 26 | 27 | @BeforeMethod 28 | public void setUpFixture() throws Exception { 29 | bucketFactory = constantTimeBucketFactory(); 30 | collector = new Collector(flusher); 31 | } 32 | 33 | @Test 34 | public void tooManyDifferentBucketsCausesFlush() throws Exception { 35 | collector.setSizeThreshold(2); 36 | 37 | Set buckets = new LinkedHashSet<>(); 38 | buckets.add(bucketFactory.forGet("here", "ns", "kindA", 1)); 39 | buckets.add(bucketFactory.forGet("here", "ns", "kindB", 1)); 40 | 41 | Iterator it = buckets.iterator(); 42 | 43 | collector.collect(it.next()); 44 | 45 | verify(flusher, never()).flush(anyCollectionOf(Bucket.class)); 46 | 47 | collector.collect(it.next()); 48 | 49 | verify(flusher).flush(buckets(buckets)); 50 | } 51 | 52 | @Test 53 | public void ageCausesFlush() throws Exception { 54 | collector.setAgeThresholdMillis(100); 55 | 56 | Bucket bucket = bucketFactory.forGet("here", "ns", "kindA", 1); 57 | 58 | collector.collect(bucket); 59 | collector.collect(bucket); 60 | 61 | verify(flusher, never()).flush(anyCollectionOf(Bucket.class)); 62 | 63 | Thread.sleep(101); 64 | collector.collect(bucket); 65 | 66 | verify(flusher).flush(buckets(bucketFactory.forGet("here", "ns", "kindA", 3))); 67 | } 68 | 69 | @Test 70 | public void sameBucketOverAndOverDoesNotCauseFlush() throws Exception { 71 | collector.setSizeThreshold(2); 72 | 73 | for (int i=0; i<10; i++) 74 | collector.collect(bucketFactory.forGet("here", "ns", "kind", 1)); 75 | 76 | verify(flusher, never()).flush(anyCollectionOf(Bucket.class)); 77 | } 78 | 79 | @Test 80 | public void sameBucketGetsAggregated() throws Exception { 81 | collector.setSizeThreshold(2); 82 | 83 | collector.collect(bucketFactory.forGet("here", "ns", "kindA", 1)); 84 | collector.collect(bucketFactory.forGet("here", "ns", "kindA", 2)); 85 | collector.collect(bucketFactory.forGet("here", "ns", "kindA", 3)); 86 | 87 | verify(flusher, never()).flush(anyCollectionOf(Bucket.class)); 88 | 89 | collector.collect(bucketFactory.forGet("here", "ns", "kindB", 1)); 90 | 91 | Set expected = new LinkedHashSet<>(); 92 | expected.add(bucketFactory.forGet("here", "ns", "kindA", 6)); 93 | expected.add(bucketFactory.forGet("here", "ns", "kindB", 1)); 94 | 95 | verify(flusher).flush(buckets(expected)); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/test/java/com/googlecode/objectify/insight/test/InsightCollectorTimeTest.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.test; 2 | 3 | import com.googlecode.objectify.insight.Bucket; 4 | import com.googlecode.objectify.insight.BucketFactory; 5 | import com.googlecode.objectify.insight.Clock; 6 | import com.googlecode.objectify.insight.Collector; 7 | import com.googlecode.objectify.insight.Flusher; 8 | import com.googlecode.objectify.insight.test.util.TestBase; 9 | import org.mockito.Mock; 10 | import org.testng.annotations.BeforeMethod; 11 | import org.testng.annotations.Test; 12 | import java.util.LinkedHashSet; 13 | import java.util.Set; 14 | import static org.mockito.Mockito.verify; 15 | import static org.mockito.Mockito.when; 16 | 17 | /** 18 | */ 19 | public class InsightCollectorTimeTest extends TestBase { 20 | 21 | @Mock private Flusher flusher; 22 | @Mock private Clock clock; 23 | 24 | private BucketFactory bucketFactory; 25 | 26 | private Collector collector; 27 | 28 | @BeforeMethod 29 | public void setUpFixture() throws Exception { 30 | when(clock.getTime()).thenReturn(100L, 200L); 31 | 32 | bucketFactory = new BucketFactory(clock, "module", "version"); 33 | collector = new Collector(flusher); 34 | } 35 | 36 | /** 37 | */ 38 | @Test 39 | public void differentTimeBucketsAreSplit() throws Exception { 40 | collector.setSizeThreshold(2); 41 | 42 | Set expected = new LinkedHashSet<>(); 43 | expected.add(bucketFactory.forGet("here", "ns", "kindA", 1)); 44 | expected.add(bucketFactory.forGet("here", "ns", "kindA", 2)); 45 | 46 | for (Bucket bucket: expected) 47 | collector.collect(bucket); 48 | 49 | verify(flusher).flush(buckets(expected)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/com/googlecode/objectify/insight/test/InsightPreparedQueryTest.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.test; 2 | 3 | import com.google.appengine.api.datastore.AsyncDatastoreService; 4 | import com.google.appengine.api.datastore.Entity; 5 | import com.google.appengine.api.datastore.FetchOptions; 6 | import com.google.appengine.api.datastore.FetchOptions.Builder; 7 | import com.google.appengine.api.datastore.Key; 8 | import com.google.appengine.api.datastore.KeyFactory; 9 | import com.google.appengine.api.datastore.PreparedQuery; 10 | import com.google.appengine.api.datastore.Query; 11 | import com.google.appengine.api.datastore.QueryResultIterable; 12 | import com.google.appengine.api.datastore.QueryResultList; 13 | import com.google.appengine.api.datastore.Transaction; 14 | import com.google.common.base.Supplier; 15 | import com.google.common.collect.Lists; 16 | import com.googlecode.objectify.insight.BucketFactory; 17 | import com.googlecode.objectify.insight.Codepointer; 18 | import com.googlecode.objectify.insight.Collector; 19 | import com.googlecode.objectify.insight.InsightAsyncDatastoreService; 20 | import com.googlecode.objectify.insight.Recorder; 21 | import com.googlecode.objectify.insight.test.util.FakeQueryResultList; 22 | import com.googlecode.objectify.insight.test.util.FakeQueryResultList.QueryResult; 23 | import com.googlecode.objectify.insight.test.util.TestBase; 24 | import org.mockito.Mock; 25 | import org.testng.annotations.BeforeMethod; 26 | import org.testng.annotations.Test; 27 | import java.util.ArrayList; 28 | import java.util.Iterator; 29 | import java.util.List; 30 | import static org.mockito.Matchers.any; 31 | import static org.mockito.Mockito.verify; 32 | import static org.mockito.Mockito.when; 33 | 34 | /** 35 | */ 36 | public class InsightPreparedQueryTest extends TestBase { 37 | 38 | private QueryResult ENTITIES; 39 | private Query QUERY; 40 | 41 | private InsightAsyncDatastoreService service; 42 | private BucketFactory bucketFactory; 43 | 44 | @Mock private Collector collector; 45 | @Mock private AsyncDatastoreService rawService; 46 | @Mock private PreparedQuery rawPq; 47 | @Mock private Codepointer codepointer; 48 | 49 | @BeforeMethod 50 | public void setUpFixture() throws Exception { 51 | bucketFactory = constantTimeBucketFactory(); 52 | 53 | when(rawService.prepare(any(Query.class))).thenReturn(rawPq); 54 | when(rawService.prepare(any(Transaction.class), any(Query.class))).thenReturn(rawPq); 55 | 56 | when(codepointer.getCodepoint()).thenReturn("here"); 57 | 58 | // Constants, but need to wait until the gae apienvironment is set up 59 | final List entities = runInNamespace("ns", new Supplier>() { 60 | @Override 61 | public List get() { 62 | List entities = new ArrayList<>(); 63 | entities.add(new Entity("Thing", 123L)); 64 | return entities; 65 | } 66 | }); 67 | ENTITIES = FakeQueryResultList.create(entities); 68 | 69 | QUERY = runInNamespace("ns", new Supplier() { 70 | @Override 71 | public Query get() { 72 | return new Query("Thing", KeyFactory.createKey("Parent", 567L)); 73 | } 74 | }); 75 | 76 | Recorder recorder = new Recorder(bucketFactory, collector, codepointer); 77 | recorder.setRecordAll(true); 78 | service = new InsightAsyncDatastoreService(rawService, recorder); 79 | } 80 | 81 | private void iterate(Iterable iterable) { 82 | iterate(iterable.iterator()); 83 | } 84 | private void iterate(Iterator iterator) { 85 | while (iterator.hasNext()) 86 | iterator.next(); 87 | } 88 | 89 | /** We can get rid of a lot of boilerplate */ 90 | private void runTest(Runnable work) { 91 | runInNamespace("ns", work); 92 | verify(collector).collect(bucketFactory.forQuery("here", "ns", "Thing", QUERY.toString(), 1)); 93 | } 94 | 95 | @Test 96 | public void queryIsCollected() throws Exception { 97 | when(rawPq.asIterable()).thenReturn(ENTITIES); 98 | 99 | runTest(new Runnable() { 100 | @Override 101 | public void run() { 102 | Iterable it = service.prepare(QUERY).asIterable(); 103 | iterate(it); 104 | iterate(it); 105 | } 106 | }); 107 | } 108 | 109 | @Test 110 | public void queryWithTxnIsCollected() throws Exception { 111 | when(rawPq.asIterable()).thenReturn(ENTITIES); 112 | 113 | runTest(new Runnable() { 114 | @Override 115 | public void run() { 116 | Iterable iterable = service.prepare(null, QUERY).asIterable(); 117 | iterate(iterable); 118 | iterate(iterable); 119 | } 120 | }); 121 | } 122 | 123 | @Test 124 | public void asQueryResultIterableIsCollected() throws Exception { 125 | when(rawPq.asQueryResultIterable()).thenReturn(ENTITIES); 126 | 127 | runTest(new Runnable() { 128 | @Override 129 | public void run() { 130 | QueryResultIterable iterable = service.prepare(QUERY).asQueryResultIterable(); 131 | iterate(iterable); 132 | iterate(iterable); 133 | } 134 | }); 135 | } 136 | 137 | @Test 138 | public void asQueryResultIterableWithOptionsIsCollected() throws Exception { 139 | when(rawPq.asQueryResultIterable(any(FetchOptions.class))).thenReturn(ENTITIES); 140 | 141 | runTest(new Runnable() { 142 | @Override 143 | public void run() { 144 | QueryResultIterable iterable = service.prepare(QUERY).asQueryResultIterable(Builder.withDefaults()); 145 | iterate(iterable); 146 | iterate(iterable); 147 | } 148 | }); 149 | } 150 | 151 | @Test 152 | public void asIterableWithOptionsIsCollected() throws Exception { 153 | when(rawPq.asIterable(any(FetchOptions.class))).thenReturn(ENTITIES); 154 | 155 | runTest(new Runnable() { 156 | @Override 157 | public void run() { 158 | Iterable iterable = service.prepare(QUERY).asIterable(Builder.withDefaults()); 159 | iterate(iterable); 160 | iterate(iterable); 161 | } 162 | }); 163 | } 164 | 165 | @Test 166 | public void asListWithOptionsIsCollected() throws Exception { 167 | when(rawPq.asList(any(FetchOptions.class))).thenReturn(ENTITIES); 168 | 169 | runTest(new Runnable() { 170 | @Override 171 | public void run() { 172 | List list = service.prepare(QUERY).asList(Builder.withDefaults()); 173 | iterate(list); 174 | iterate(list); 175 | } 176 | }); 177 | } 178 | 179 | @Test 180 | public void asQueryResultListWithOptionsIsCollected() throws Exception { 181 | when(rawPq.asQueryResultList(any(FetchOptions.class))).thenReturn(ENTITIES); 182 | 183 | runTest(new Runnable() { 184 | @Override 185 | public void run() { 186 | QueryResultList list = service.prepare(QUERY).asQueryResultList(Builder.withDefaults()); 187 | iterate(list); 188 | iterate(list); 189 | } 190 | }); 191 | } 192 | 193 | @Test 194 | public void asIteratorIsCollected() throws Exception { 195 | when(rawPq.asIterator()).thenReturn(ENTITIES.iterator()); 196 | 197 | runTest(new Runnable() { 198 | @Override 199 | public void run() { 200 | iterate(service.prepare(QUERY).asIterator()); 201 | } 202 | }); 203 | } 204 | 205 | @Test 206 | public void asIteratorWithOptionsIsCollected() throws Exception { 207 | when(rawPq.asIterator(any(FetchOptions.class))).thenReturn(ENTITIES.iterator()); 208 | 209 | runTest(new Runnable() { 210 | @Override 211 | public void run() { 212 | iterate(service.prepare(QUERY).asIterator(FetchOptions.Builder.withDefaults())); 213 | } 214 | }); 215 | } 216 | 217 | @Test 218 | public void asQueryResultIteratorIsCollected() throws Exception { 219 | when(rawPq.asQueryResultIterator()).thenReturn(ENTITIES.iterator()); 220 | 221 | runTest(new Runnable() { 222 | @Override 223 | public void run() { 224 | iterate(service.prepare(QUERY).asQueryResultIterator()); 225 | } 226 | }); 227 | } 228 | 229 | @Test 230 | public void asQueryResultIteratorWithOptionsIsCollected() throws Exception { 231 | when(rawPq.asQueryResultIterator(any(FetchOptions.class))).thenReturn(ENTITIES.iterator()); 232 | 233 | runTest(new Runnable() { 234 | @Override 235 | public void run() { 236 | iterate(service.prepare(QUERY).asQueryResultIterator(FetchOptions.Builder.withDefaults())); 237 | } 238 | }); 239 | } 240 | 241 | @Test 242 | public void asSingleEntityIsCollected() throws Exception { 243 | when(rawPq.asSingleEntity()).thenReturn(ENTITIES.iterator().next()); 244 | 245 | runTest(new Runnable() { 246 | @Override 247 | public void run() { 248 | service.prepare(QUERY).asSingleEntity(); 249 | } 250 | }); 251 | } 252 | 253 | @Test 254 | public void listToArrayIsCollected() throws Exception { 255 | when(rawPq.asList(any(FetchOptions.class))).thenReturn(ENTITIES); 256 | 257 | runTest(new Runnable() { 258 | @Override 259 | public void run() { 260 | List list = service.prepare(QUERY).asList(Builder.withDefaults()); 261 | list.toArray(); 262 | list.toArray(); 263 | } 264 | }); 265 | } 266 | 267 | @Test 268 | public void queryResultListToArrayIsCollected() throws Exception { 269 | when(rawPq.asQueryResultList(any(FetchOptions.class))).thenReturn(ENTITIES); 270 | 271 | runTest(new Runnable() { 272 | @Override 273 | public void run() { 274 | List list = service.prepare(QUERY).asQueryResultList(Builder.withDefaults()); 275 | list.toArray(); 276 | list.toArray(); 277 | } 278 | }); 279 | } 280 | 281 | @Test 282 | public void listToArray2IsCollected() throws Exception { 283 | when(rawPq.asList(any(FetchOptions.class))).thenReturn(ENTITIES); 284 | 285 | runTest(new Runnable() { 286 | @Override 287 | public void run() { 288 | List list = service.prepare(QUERY).asList(Builder.withDefaults()); 289 | list.toArray(new Entity[0]); 290 | list.toArray(new Entity[0]); 291 | } 292 | }); 293 | } 294 | 295 | @Test 296 | public void queryResultListToArray2IsCollected() throws Exception { 297 | when(rawPq.asQueryResultList(any(FetchOptions.class))).thenReturn(ENTITIES); 298 | 299 | runTest(new Runnable() { 300 | @Override 301 | public void run() { 302 | List list = service.prepare(QUERY).asQueryResultList(Builder.withDefaults()); 303 | list.toArray(new Entity[0]); 304 | list.toArray(new Entity[0]); 305 | } 306 | }); 307 | } 308 | 309 | } 310 | -------------------------------------------------------------------------------- /src/test/java/com/googlecode/objectify/insight/test/RecorderTest.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.test; 2 | 3 | import com.google.appengine.api.datastore.AsyncDatastoreService; 4 | import com.google.appengine.api.datastore.Key; 5 | import com.google.appengine.api.datastore.KeyFactory; 6 | import com.googlecode.objectify.insight.Bucket; 7 | import com.googlecode.objectify.insight.BucketFactory; 8 | import com.googlecode.objectify.insight.Codepointer; 9 | import com.googlecode.objectify.insight.Collector; 10 | import com.googlecode.objectify.insight.InsightAsyncDatastoreService; 11 | import com.googlecode.objectify.insight.Recorder; 12 | import com.googlecode.objectify.insight.test.util.TestBase; 13 | import org.mockito.Mock; 14 | import org.testng.annotations.BeforeMethod; 15 | import org.testng.annotations.Test; 16 | import static org.mockito.Matchers.any; 17 | import static org.mockito.Mockito.never; 18 | import static org.mockito.Mockito.verify; 19 | import static org.mockito.Mockito.when; 20 | 21 | /** 22 | * Make sure the recorder is behaving appropriately. 23 | */ 24 | public class RecorderTest extends TestBase { 25 | 26 | private InsightAsyncDatastoreService service; 27 | private BucketFactory bucketFactory; 28 | private Recorder recorder; 29 | 30 | @Mock private AsyncDatastoreService raw; 31 | @Mock private Collector collector; 32 | @Mock private Codepointer codepointer; 33 | 34 | @BeforeMethod 35 | public void setUpFixture() throws Exception { 36 | bucketFactory = constantTimeBucketFactory(); 37 | 38 | when(codepointer.getCodepoint()).thenReturn("here"); 39 | 40 | recorder = new Recorder(bucketFactory, collector, codepointer); 41 | 42 | service = new InsightAsyncDatastoreService(raw, recorder); 43 | } 44 | 45 | @Test 46 | public void noRecording() throws Exception { 47 | runInNamespace("ns", new Runnable() { 48 | @Override 49 | public void run() { 50 | Key key = KeyFactory.createKey("Thing", 123L); 51 | service.get(key); 52 | } 53 | }); 54 | 55 | verify(collector, never()).collect(any(Bucket.class)); 56 | } 57 | 58 | @Test 59 | public void recordsKind() throws Exception { 60 | recorder.recordKind("Thing"); 61 | 62 | runInNamespace("ns", new Runnable() { 63 | @Override 64 | public void run() { 65 | Key key = KeyFactory.createKey("Thing", 123L); 66 | service.get(key); 67 | } 68 | }); 69 | 70 | verify(collector).collect(bucketFactory.forGet("here", "ns", "Thing", 1)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/java/com/googlecode/objectify/insight/test/util/BucketsMatcher.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.test.util; 2 | 3 | import com.googlecode.objectify.insight.Bucket; 4 | import lombok.RequiredArgsConstructor; 5 | import org.hamcrest.Description; 6 | import org.mockito.ArgumentMatcher; 7 | import java.util.Collection; 8 | import java.util.LinkedHashSet; 9 | import java.util.Set; 10 | 11 | /** 12 | * Hamcrest matcher for exactly matching a collection of buckets which are matched by their key. 13 | * Read/write counts are checked. 14 | */ 15 | @RequiredArgsConstructor 16 | public class BucketsMatcher extends ArgumentMatcher> { 17 | private final Set patternSet; 18 | 19 | public BucketsMatcher(Collection pattern) { 20 | patternSet = new LinkedHashSet<>(pattern); 21 | } 22 | 23 | @Override 24 | public boolean matches(Object o) { 25 | @SuppressWarnings("unchecked") 26 | Collection other = (Collection)o; 27 | Set otherSet = new LinkedHashSet<>(other); 28 | 29 | return otherSet.equals(patternSet); 30 | } 31 | 32 | @Override 33 | public void describeTo(Description description) { 34 | description.appendText(patternSet.toString()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/googlecode/objectify/insight/test/util/FakeQueryResultList.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.test.util; 2 | 3 | import com.google.appengine.api.datastore.Entity; 4 | import com.google.appengine.api.datastore.QueryResultIterable; 5 | import com.google.appengine.api.datastore.QueryResultIterator; 6 | import com.google.appengine.api.datastore.QueryResultList; 7 | import com.googlecode.objectify.insight.util.ReflectionUtils; 8 | import lombok.RequiredArgsConstructor; 9 | import java.lang.reflect.InvocationHandler; 10 | import java.lang.reflect.Method; 11 | import java.lang.reflect.Proxy; 12 | import java.util.List; 13 | 14 | /** 15 | * Fake list, implemented as a proxy 16 | */ 17 | @RequiredArgsConstructor 18 | public class FakeQueryResultList implements InvocationHandler { 19 | public static interface QueryResult extends QueryResultList, QueryResultIterable {} 20 | 21 | private static final Method ITERATOR_METHOD = ReflectionUtils.getMethod(List.class, "iterator"); 22 | 23 | private final List list; 24 | 25 | @Override 26 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 27 | Object result = method.invoke(list, args); 28 | 29 | if (method.equals(ITERATOR_METHOD)) 30 | return PassThroughProxy.create(result, QueryResultIterator.class); 31 | else 32 | return result; 33 | } 34 | 35 | public static QueryResult create(List list) { 36 | return (QueryResult)Proxy.newProxyInstance(QueryResult.class.getClassLoader(), new Class[]{QueryResult.class}, new FakeQueryResultList(list)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/com/googlecode/objectify/insight/test/util/PassThroughProxy.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.test.util; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import java.lang.reflect.InvocationHandler; 5 | import java.lang.reflect.Method; 6 | import java.lang.reflect.Proxy; 7 | 8 | /** 9 | * Simple pass through proxy, lets us work around the type system 10 | */ 11 | @RequiredArgsConstructor 12 | public class PassThroughProxy implements InvocationHandler { 13 | 14 | private final Object thing; 15 | 16 | @Override 17 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 18 | return method.invoke(thing, args); 19 | } 20 | 21 | public static T create(Object thing, Class intf) { 22 | final Class[] interfaces = { intf }; 23 | return (T)Proxy.newProxyInstance(intf.getClassLoader(), interfaces, new PassThroughProxy(thing)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/com/googlecode/objectify/insight/test/util/TestBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | */ 3 | 4 | package com.googlecode.objectify.insight.test.util; 5 | 6 | import com.fasterxml.jackson.core.JsonProcessingException; 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import com.google.appengine.api.NamespaceManager; 9 | import com.google.appengine.api.taskqueue.TaskHandle; 10 | import com.google.appengine.api.taskqueue.TaskOptions; 11 | import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig; 12 | import com.google.appengine.tools.development.testing.LocalServiceTestHelper; 13 | import com.google.appengine.tools.development.testing.LocalTaskQueueTestConfig; 14 | import com.google.common.base.Supplier; 15 | import com.googlecode.objectify.insight.Bucket; 16 | import com.googlecode.objectify.insight.BucketFactory; 17 | import com.googlecode.objectify.insight.Clock; 18 | import com.googlecode.objectify.insight.Flusher; 19 | import org.mockito.MockitoAnnotations; 20 | import org.testng.annotations.AfterMethod; 21 | import org.testng.annotations.BeforeMethod; 22 | import java.util.Collection; 23 | import java.util.Collections; 24 | import static org.mockito.Matchers.argThat; 25 | import static org.mockito.Mockito.mock; 26 | import static org.mockito.Mockito.when; 27 | 28 | /** 29 | * All tests should extend this class to set up the GAE environment. 30 | * @see Unit Testing in Appengine 31 | * 32 | * Also sets up any Mockito annotated fields. 33 | * 34 | * @author Jeff Schnitzer 35 | */ 36 | public class TestBase 37 | { 38 | /** */ 39 | private final LocalServiceTestHelper helper = 40 | new LocalServiceTestHelper( 41 | // Our tests assume strong consistency 42 | new LocalDatastoreServiceTestConfig().setApplyAllHighRepJobPolicy(), 43 | new LocalTaskQueueTestConfig()); 44 | /** */ 45 | @BeforeMethod 46 | public void setUp() { 47 | this.helper.setUp(); 48 | MockitoAnnotations.initMocks(this); 49 | } 50 | 51 | /** */ 52 | @AfterMethod 53 | public void tearDown() { 54 | this.helper.tearDown(); 55 | } 56 | 57 | /** */ 58 | protected void runInNamespace(String namespace, Runnable runnable) { 59 | String oldNamespace = NamespaceManager.get(); 60 | try { 61 | NamespaceManager.set(namespace); 62 | 63 | runnable.run(); 64 | } finally { 65 | NamespaceManager.set(oldNamespace); 66 | } 67 | } 68 | 69 | /** */ 70 | protected T runInNamespace(String namespace, Supplier supplier) { 71 | String oldNamespace = NamespaceManager.get(); 72 | try { 73 | NamespaceManager.set(namespace); 74 | 75 | return supplier.get(); 76 | } finally { 77 | NamespaceManager.set(oldNamespace); 78 | } 79 | } 80 | 81 | /** Little bit of boilerplate that makes the tests read better */ 82 | protected Collection buckets(Collection matching) { 83 | return argThat(new BucketsMatcher(matching)); 84 | } 85 | 86 | /** Little bit of boilerplate that makes the tests read better */ 87 | protected Collection buckets(Bucket singletonSetContent) { 88 | return buckets(Collections.singleton(singletonSetContent)); 89 | } 90 | 91 | /** Convenience method */ 92 | protected byte[] jsonify(Object object) throws JsonProcessingException { 93 | return new ObjectMapper().writeValueAsBytes(object); 94 | } 95 | 96 | /** 97 | * Make a task handle which holds the jsonified payload. Task name is an arbitrary unique string. 98 | */ 99 | protected TaskHandle makeTaskHandle(Object payload) throws JsonProcessingException { 100 | return new TaskHandle( 101 | TaskOptions.Builder.withPayload(jsonify(payload), "application/json").taskName(makeUniqueString()), 102 | Flusher.DEFAULT_QUEUE); 103 | } 104 | 105 | protected String makeUniqueString() { 106 | return new Object().toString().split("@")[1]; 107 | } 108 | 109 | 110 | /** Useful for making stable tests */ 111 | protected BucketFactory constantTimeBucketFactory() { 112 | Clock clock = mock(Clock.class); 113 | when(clock.getTime()).thenReturn(100L); // just a stable value 114 | return new BucketFactory(clock, "module", "version"); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/com/googlecode/objectify/insight/util/test/StackTraceUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.googlecode.objectify.insight.util.test; 2 | 3 | import static org.hamcrest.Matchers.equalTo; 4 | import static org.junit.Assert.assertThat; 5 | 6 | import org.testng.annotations.Test; 7 | 8 | import com.googlecode.objectify.insight.util.StackTraceUtils; 9 | 10 | public class StackTraceUtilsTest { 11 | 12 | private static String nonEnhanced = "at com.googlecode.objectify.insight.StackTracer.stack(StackTracer.java:16)\r\n" 13 | + "\tat com.googlecode.objectify.insight.StackTracerTest.stack(StackTracerTest.java:12)\r\n" 14 | + "\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\r\n" 15 | + "\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)\r\n" 16 | + "\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\r\n" 17 | + "\tat java.lang.reflect.Method.invoke(Method.java:606)"; 18 | 19 | private static String enhanced = "at com.googlecode.objectify.insight.Codepointer.getCodepoint(Codepointer.java:40)\r\n" 20 | + "\tat com.googlecode.objectify.insight.test.CodepointTestContext.codepoint(CodepointTestContext.java:17)\r\n" 21 | + "\tat com.googlecode.objectify.insight.test.CodepointTestContext$$EnhancerByCGLIB$$4b065412.CGLIB$codepoint$0()\r\n" 22 | + "\tat com.googlecode.objectify.insight.test.CodepointTestContext$$EnhancerByCGLIB$$4b065412$$FastClassByCGLIB$$b6c1cce6.invoke()\r\n" 23 | + "\tat net.sf.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:228)\r\n" 24 | + "\tat com.googlecode.objectify.insight.test.CodepointerTest$2.intercept(CodepointerTest.java:86)\r\n" 25 | + "\tat com.googlecode.objectify.insight.test.CodepointTestContext$$EnhancerByCGLIB$$4b065412.codepoint()\r\n" 26 | + "\tat com.googlecode.objectify.insight.test.CodepointerTest.enhancedClassesShouldGenerateSameCodepointHash(CodepointerTest.java:46)\r\n" 27 | + "\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\r\n" 28 | + "\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)\r\n" 29 | + "\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\r\n" 30 | + "\tat java.lang.reflect.Method.invoke(Method.java:606)"; 31 | 32 | private static String unEnhanced = "at com.googlecode.objectify.insight.Codepointer.getCodepoint(Codepointer.java:40)\r\n" 33 | + "\tat com.googlecode.objectify.insight.test.CodepointTestContext.codepoint(CodepointTestContext.java:17)\r\n" 34 | + "\tat com.googlecode.objectify.insight.test.CodepointTestContext$$EnhancerByCGLIB$$.CGLIB$codepoint$0()\r\n" 35 | + "\tat com.googlecode.objectify.insight.test.CodepointTestContext$$EnhancerByCGLIB$$$$FastClassByCGLIB$$.invoke()\r\n" 36 | + "\tat net.sf.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:228)\r\n\tat com.googlecode.objectify.insight.test.CodepointerTest$2.intercept(CodepointerTest.java:86)\r\n" 37 | + "\tat com.googlecode.objectify.insight.test.CodepointTestContext$$EnhancerByCGLIB$$.codepoint()\r\n" 38 | + "\tat com.googlecode.objectify.insight.test.CodepointerTest.enhancedClassesShouldGenerateSameCodepointHash(CodepointerTest.java:46)\r\n" 39 | + "\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\r\n" 40 | + "\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)\r\n" 41 | + "\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\r\n" 42 | + "\tat java.lang.reflect.Method.invoke(Method.java:606)"; 43 | 44 | @Test 45 | public void nonEnhancedStackTraceShouldRemainTheSame() { 46 | assertThat(StackTraceUtils.removeMutableEnhancements(nonEnhanced), equalTo(nonEnhanced)); 47 | } 48 | 49 | @Test 50 | public void enhancedStacktraceShouldHaveDynamicPartsRemoved() { 51 | assertThat(StackTraceUtils.removeMutableEnhancements(enhanced), equalTo(unEnhanced)); 52 | } 53 | 54 | } 55 | --------------------------------------------------------------------------------