Skip to content

Commit

Permalink
Embellishments to stat publishing:
Browse files Browse the repository at this point in the history
1) adding mapbinder-based configuration of StatsServlet
 > this facilitates application-specific additions of publishers
 
 2) publishing a richer snapshot of data
  > moves from Map<String, String> to Map<StatDescriptor, Object>
  > this gives each publisher the name, description, and java value of a stat
  
 3) adding tests for the new publishers
  
  Note: the description field is yet unused, but it's clear how it
  can now be used by a publisher, versus having been hidden before.

Dependency changes:
* this CL migrates from google-collections to guava
* this CL introduces a dependency on google gson

IML files:
Note, because I'm using IJ 10, I'm not sure that my local
edits are going to be backwards compatible.  I left them out of this
CL.  This means that the guava and gson deps will need to be added
to the .iml files for intellij to pick up the new deps (I'm not sure what
the best thing to do here would have been).

git-svn-id: http://google-sitebricks.googlecode.com/svn/trunk@196 c7392d14-1f41-11de-a108-cd2f117ce590
  • Loading branch information
ffaber committed Jan 12, 2011
1 parent 4ffaeb0 commit c7f596f
Show file tree
Hide file tree
Showing 14 changed files with 537 additions and 102 deletions.
8 changes: 4 additions & 4 deletions sitebricks/pom.xml
Expand Up @@ -36,10 +36,10 @@
<version>2.0</version>
</dependency>
<dependency>
<groupId>com.google.collections</groupId>
<artifactId>google-collections</artifactId>
<version>1.0-rc2</version>
</dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>r07</version>
</dependency>
<dependency>
<groupId>net.jcip</groupId>
<artifactId>jcip-annotations</artifactId>
Expand Down
30 changes: 26 additions & 4 deletions stat/pom.xml
Expand Up @@ -19,6 +19,19 @@
<url>http://code.google.com/p/google-sitebricks/source/browse</url>
</scm>

<repositories>
<repository>
<id>gson</id>
<url>http://google-gson.googlecode.com/svn/mavenrepo</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>

<dependencies>
<dependency>
<groupId>junit</groupId>
Expand All @@ -32,9 +45,19 @@
<version>2.0</version>
</dependency>
<dependency>
<groupId>com.google.collections</groupId>
<artifactId>google-collections</artifactId>
<version>1.0-rc2</version>
<groupId>com.google.inject.extensions</groupId>
<artifactId>guice-multibindings</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>1.6</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>r07</version>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
Expand Down Expand Up @@ -79,5 +102,4 @@
<url> https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>

</project>
51 changes: 51 additions & 0 deletions stat/src/main/java/com/google/inject/stat/HtmlStatsPublisher.java
@@ -0,0 +1,51 @@
/**
* Copyright (C) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package com.google.inject.stat;

import com.google.common.collect.ImmutableMap;

import java.io.PrintWriter;
import java.util.Map;

/**
* This implementation of {@link StatsPublisher} publishes snapshots as html.
*
* @author ffaber@gmail.com (Fred Faber)
*/
class HtmlStatsPublisher extends StatsPublisher {

@Override String getContentType() {
return "text/html";
}

@Override void publish(
ImmutableMap<StatDescriptor, Object> snapshot, PrintWriter writer) {
writer.println("<html><head><style>");
writer.println("body { font-family: monospace; }");
writer.println("</style></head><body>");
for (Map.Entry<StatDescriptor, Object> entry : snapshot.entrySet()) {
StatDescriptor statDescriptor = entry.getKey();
writer.print("<b>");
writer.print(statDescriptor.getName());
writer.print(":</b> ");
writer.print(entry.getValue());
writer.println("<br/>");
}
writer.println("</body></html>");
}
}
50 changes: 50 additions & 0 deletions stat/src/main/java/com/google/inject/stat/JsonStatsPublisher.java
@@ -0,0 +1,50 @@
/**
* Copyright (C) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package com.google.inject.stat;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.gson.Gson;

import java.io.PrintWriter;
import java.util.Map;
import java.util.Map.Entry;

/**
* This implementation of {@link StatsPublisher} publishes a snapshot as JSON.
*
* @author ffaber@gmail.com (Fred Faber)
*/
class JsonStatsPublisher extends StatsPublisher {

@Override String getContentType() {
return "application/json";
}

@Override void publish(
ImmutableMap<StatDescriptor, Object> snapshot, PrintWriter writer) {
Gson gson = new Gson();
Map<String, Object> valuesByName = Maps.newLinkedHashMap();
for (Entry<StatDescriptor, Object> entry : snapshot.entrySet()) {
valuesByName.put(entry.getKey().getName(), entry.getValue());
}

String gsonString = gson.toJson(valuesByName);
writer.write(gsonString);
}
}
Expand Up @@ -13,7 +13,7 @@
class StatAnnotatedTypeListener implements TypeListener {
private final Stats stats;

public StatAnnotatedTypeListener(Stats stats) {
StatAnnotatedTypeListener(Stats stats) {
this.stats = stats;
}

Expand All @@ -26,7 +26,8 @@ public <I> void hear(TypeLiteral<I> type, TypeEncounter<I> encounter) {
encounter.register(new InjectionListener<I>() {
@Override
public void afterInjection(I injectee) {
stats.register(new StatDescriptor(injectee, stat.value(), field));
stats.register(new StatDescriptor(
injectee, stat.value(), stat.description(), field));
}
});
}
Expand Down
40 changes: 16 additions & 24 deletions stat/src/main/java/com/google/inject/stat/StatDescriptor.java
Expand Up @@ -5,41 +5,33 @@
/**
* @author dhanji@gmail.com (Dhanji R. Prasanna)
*/
class StatDescriptor {
private final Object instance;
private final String stat;
public final class StatDescriptor {
private final Object target;
private final String name;
private final String description;
private final Field field;

StatDescriptor(Object instance, String stat, Field field) {
this.instance = instance;
this.stat = stat;
public StatDescriptor(
Object target, String name, String description, Field field) {
this.target = target;
this.name = name;
this.description = description;
this.field = field;
}

if (!field.isAccessible()) {
field.setAccessible(true);
}
public String getName() {
return name;
}

public String getStat() {
return stat;
public String getDescription() {
return description;
}

public Object getInstance() {
return instance;
public Object getTarget() {
return target;
}

public Field getField() {
return field;
}

public String read() {
Object value = null;
try {
value = field.get(instance);
} catch (IllegalAccessException e) {
return "unable to read: " + e.getMessage();
}

return value.toString();
}
}
86 changes: 82 additions & 4 deletions stat/src/main/java/com/google/inject/stat/StatModule.java
@@ -1,20 +1,91 @@
package com.google.inject.stat;

import com.google.common.base.Preconditions;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.inject.stat.StatsServlet.DEFAULT_FORMAT;

import com.google.inject.matcher.Matchers;
import com.google.inject.multibindings.MapBinder;
import com.google.inject.servlet.ServletModule;

/**
* Module to install which enables statistics tracking and monitoring capabilities.
* This module enables publishing values annotated with {@link Stat} to a given
* servlet path.
* <p>
* As an example, consider the following class:
* <pre><code>
* class QueryServlet extends HttpServlet {
* {@literal @}Stat("search-hits")
* private final AtomicInteger hits = new AtomicInteger(0);
*
* {@literal @}Inject QueryServlet(....) { }
*
* {@literal @}Override void doGet(
* HttpServletRequest req, HttpServletResponse resp) {
* ....
* String searchTerm = req.getParameter("q");
* SearchResult result = searchService.searchFor(searchTerm);
* if (result.hasHits()) {
* hits.incrementAndGet();
* }
* ...
* }
* }
* </pre></code>
* <p>
* This class exports a stat called {@code search-hits}. To configure the
* server to publish this stat, install a {@link StatModule}, such as:
* <pre><code>
* public class YourServerModule extends AbstractModule {
* {@literal @}Override protected void configure() {
* install(new StatsModule("/stats");
* ...
* }
* }
* </code></pre>
* <p>
* Then, to query the server for its stats, hit the url that was registered
* with the module (which was {@code /stats}, in the example above).
*
* <h3>Published Formats</h3>
* By default, published stats are available in several formats:
* <ul>
* <li>html - a formatted html page
* <li>json - well formed json
* <li>text - simple plaintext page
* </ul>
* To request stats in a given format, include a value for the
* {@value StatsServlet#DEFAULT_FORMAT} parameter in the {@code /stats} request.
* For the formats above, the value for this parameter should correspond to the
* type of output (i.e., pass "html", "json", or "text"). If no parameter is
* given, then html is returned.
* <p>
* <h3>Extensions</h3>
* You may extend the default set of publishers by adding and binding another
* implementation of {@link StatsPublisher}. To add your implementation,
* add a binding to a {@link MapBinder MapBinder&lt;String, StatsPublisher&gt;}.
* For example:
* <pre><code>
* public class CustomPublisherModule extends AbstractModule {
* {@literal @}Override protected void configure() {
* MapBinder&lt;String, StatsPublisher&gt; mapBinder =
* MapBinder.newMapBinder(binder(), String.class, StatsPublisher.class);
* mapBinder.addBinding("custom").to(CustomStatsPublisher.class);
* }
* }
* </code></pre>
* You can then retrieve stats from your custom publisher by hitting
* {@code /stats?format=custom}.
*
* @author dhanji@gmail.com (Dhanji R. Prasanna)
* @author ffaber@gmail.com (Fred Faber)
*/
public class StatModule extends ServletModule {
private final String uriPath;

public StatModule(String uriPath) {
Preconditions.checkArgument(null != uriPath && !uriPath.isEmpty(),
"URI path must be a valid non-empty servlet path mapping (example: /debug)");
checkArgument(!isNullOrEmpty(uriPath),
"URI path must be a valid non-empty servlet path mapping (example: /stats)");
this.uriPath = uriPath;
}

Expand All @@ -26,5 +97,12 @@ protected void configureServlets() {
bind(Stats.class).toInstance(stats);

serve(uriPath).with(StatsServlet.class);

MapBinder<String, StatsPublisher> publisherBinder =
MapBinder.newMapBinder(binder(), String.class, StatsPublisher.class);
publisherBinder.addBinding(DEFAULT_FORMAT).to(HtmlStatsPublisher.class);
publisherBinder.addBinding("html").to(HtmlStatsPublisher.class);
publisherBinder.addBinding("json").to(JsonStatsPublisher.class);
publisherBinder.addBinding("text").to(TextStatsPublisher.class);
}
}

0 comments on commit c7f596f

Please sign in to comment.