devapp, godash: collect issue statistics for plotting

A subsequent CL will add a dashboard for graphing project health; this
CL collects information about when issues are opened, closed, and
milestoned.

This also includes a refactor of the guts of devapp to make it more
modular.

It also adds logging to the godash library, which can be activated with
the -v flag in the godash command.

Change-Id: I54c1419435e496f5c0e2e7f4b966b3ee4de0b0a1
Reviewed-on: https://go-review.googlesource.com/28091
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/devapp/cache.go b/devapp/cache.go
new file mode 100644
index 0000000..dfbd5d5
--- /dev/null
+++ b/devapp/cache.go
@@ -0,0 +1,85 @@
+// Copyright 2016 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package devapp
+
+import (
+	"bytes"
+	"compress/gzip"
+	"encoding/gob"
+
+	"golang.org/x/net/context"
+	"google.golang.org/appengine/datastore"
+	"google.golang.org/appengine/log"
+)
+
+// Cache is a datastore entity type that contains serialized data for dashboards.
+type Cache struct {
+	// Value contains a gzipped gob'd serialization of the object
+	// to be cached. It must be []byte to avail ourselves of the
+	// datastore's 1 MB size limit.
+	Value []byte
+}
+
+func getCaches(ctx context.Context, names ...string) map[string]*Cache {
+	out := make(map[string]*Cache)
+	var keys []*datastore.Key
+	var ptrs []*Cache
+	for _, name := range names {
+		keys = append(keys, datastore.NewKey(ctx, entityPrefix+"Cache", name, 0, nil))
+		out[name] = &Cache{}
+		ptrs = append(ptrs, out[name])
+	}
+	datastore.GetMulti(ctx, keys, ptrs) // Ignore errors since they might not exist.
+	return out
+}
+
+func getCache(ctx context.Context, name string) (*Cache, error) {
+	var cache Cache
+	if err := datastore.Get(ctx, datastore.NewKey(ctx, entityPrefix+"Cache", name, 0, nil), &cache); err != nil {
+		return nil, err
+	}
+	return &cache, nil
+}
+
+func unpackCache(cache *Cache, data interface{}) error {
+	if len(cache.Value) > 0 {
+		gzr, err := gzip.NewReader(bytes.NewReader(cache.Value))
+		if err != nil {
+			return err
+		}
+		defer gzr.Close()
+		if err := gob.NewDecoder(gzr).Decode(data); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func loadCache(ctx context.Context, name string, data interface{}) error {
+	cache, err := getCache(ctx, name)
+	if err != nil {
+		return err
+	}
+	return unpackCache(cache, data)
+}
+
+func writeCache(ctx context.Context, name string, data interface{}) error {
+	var cache Cache
+	var cacheout bytes.Buffer
+	cachegz := gzip.NewWriter(&cacheout)
+	e := gob.NewEncoder(cachegz)
+	if err := e.Encode(data); err != nil {
+		return err
+	}
+	if err := cachegz.Close(); err != nil {
+		return err
+	}
+	cache.Value = cacheout.Bytes()
+	log.Infof(ctx, "Cache %q update finished; writing %d bytes", name, cacheout.Len())
+	if _, err := datastore.Put(ctx, datastore.NewKey(ctx, entityPrefix+"Cache", name, 0, nil), &cache); err != nil {
+		return err
+	}
+	return nil
+}