-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #88 from krancour/add-redis-adapter
feat(storage): Add redis storage adapter
- Loading branch information
Showing
8 changed files
with
297 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package redis | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
|
||
r "gopkg.in/redis.v3" | ||
) | ||
|
||
type adapter struct { | ||
bufferSize int | ||
redisClient *r.Client | ||
} | ||
|
||
// NewStorageAdapter returns a pointer to a new instance of a redis-based storage.Adapter. | ||
func NewStorageAdapter(bufferSize int) (*adapter, error) { | ||
if bufferSize <= 0 { | ||
return nil, fmt.Errorf("Invalid buffer size: %d", bufferSize) | ||
} | ||
cfg, err := parseConfig(appName) | ||
if err != nil { | ||
log.Fatalf("config error: %s: ", err) | ||
} | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &adapter{ | ||
bufferSize: bufferSize, | ||
redisClient: r.NewClient(&r.Options{ | ||
Addr: fmt.Sprintf("%s:%d", cfg.RedisHost, cfg.RedisPort), | ||
Password: cfg.RedisPassword, // "" == no password | ||
DB: int64(cfg.RedisDB), | ||
}), | ||
}, nil | ||
} | ||
|
||
// Write adds a log message to to an app-specific list in redis using ring-buffer-like semantics | ||
func (a *adapter) Write(app string, message string) error { | ||
// Note: Deliberately NOT using MULTI / transactions here since in this implementation of the | ||
// redis client, MULTI is not safe for concurrent use by multiple goroutines. It's been advised | ||
// by the authors of the gopkg.in/redis.v3 package to just use pipelining when possible... | ||
// and here that is technically possible. In the WORST case scenario, not having transactions | ||
// means we may momentarily have more than the desired number of log entries in the list / | ||
// buffer, but an LTRIM will eventually correct that, bringing the list / buffer back down to | ||
// its desired max size. | ||
pipeline := a.redisClient.Pipeline() | ||
if err := pipeline.RPush(app, message).Err(); err != nil { | ||
return err | ||
} | ||
if err := pipeline.LTrim(app, int64(-1*a.bufferSize), -1).Err(); err != nil { | ||
return err | ||
} | ||
if _, err := pipeline.Exec(); err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
// Read retrieves a specified number of log lines from an app-specific list in redis | ||
func (a *adapter) Read(app string, lines int) ([]string, error) { | ||
stringSliceCmd := a.redisClient.LRange(app, int64(-1*lines), -1) | ||
result, err := stringSliceCmd.Result() | ||
if err != nil { | ||
return nil, err | ||
} | ||
if len(result) > 0 { | ||
return result, nil | ||
} | ||
return nil, fmt.Errorf("Could not find logs for '%s'", app) | ||
} | ||
|
||
// Destroy deletes an app-specific list from redis | ||
func (a *adapter) Destroy(app string) error { | ||
if err := a.redisClient.Del(app).Err(); err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
func (a *adapter) Reopen() error { | ||
// No-op | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
package redis | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
) | ||
|
||
const app string = "test-app" | ||
|
||
func TestReadFromNonExistingApp(t *testing.T) { | ||
// Initialize a new storage adapter | ||
a, err := NewStorageAdapter(10) | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
// No logs have been written; there should be no redis list for app | ||
messages, err := a.Read(app, 10) | ||
if messages != nil { | ||
t.Error("Expected no messages, but got some") | ||
} | ||
if err == nil || err.Error() != fmt.Sprintf("Could not find logs for '%s'", app) { | ||
t.Error("Did not receive expected error message") | ||
} | ||
} | ||
|
||
func TestWithBadBufferSizes(t *testing.T) { | ||
// Initialize with invalid buffer sizes | ||
for _, size := range []int{-1, 0} { | ||
a, err := NewStorageAdapter(size) | ||
if a != nil { | ||
t.Error("Expected no storage adapter, but got one") | ||
} | ||
if err == nil || err.Error() != fmt.Sprintf("Invalid buffer size: %d", size) { | ||
t.Error("Did not receive expected error message") | ||
} | ||
} | ||
} | ||
|
||
func TestLogs(t *testing.T) { | ||
// Initialize with small buffers | ||
a, err := NewStorageAdapter(10) | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
// And write a few logs to it, but do NOT fill it up | ||
for i := 0; i < 5; i++ { | ||
if err := a.Write(app, fmt.Sprintf("message %d", i)); err != nil { | ||
t.Error(err) | ||
} | ||
} | ||
// Read more logs than there are | ||
messages, err := a.Read(app, 8) | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
// Should only get as many messages as we actually have | ||
if len(messages) != 5 { | ||
t.Errorf("only expected 5 log messages, got %d", len(messages)) | ||
} | ||
// Read fewer logs than there are | ||
messages, err = a.Read(app, 3) | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
// Should get the 3 MOST RECENT logs | ||
if len(messages) != 3 { | ||
t.Errorf("only expected 5 log messages, got %d", len(messages)) | ||
} | ||
for i := 0; i < 3; i++ { | ||
expectedMessage := fmt.Sprintf("message %d", i+2) | ||
if messages[i] != expectedMessage { | ||
t.Errorf("expected: \"%s\", got \"%s\"", expectedMessage, messages[i]) | ||
} | ||
} | ||
// Overfill the buffer | ||
for i := 5; i < 11; i++ { | ||
if err := a.Write(app, fmt.Sprintf("message %d", i)); err != nil { | ||
t.Error(err) | ||
} | ||
} | ||
// Read more logs than the buffer can hold | ||
messages, err = a.Read(app, 20) | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
// Should only get as many messages as the buffer can hold | ||
if len(messages) != 10 { | ||
t.Errorf("only expected 10 log messages, got %d", len(messages)) | ||
} | ||
// And they should only be the 10 MOST RECENT logs | ||
for i := 0; i < 10; i++ { | ||
expectedMessage := fmt.Sprintf("message %d", i+1) | ||
if messages[i] != expectedMessage { | ||
t.Errorf("expected: \"%s\", got \"%s\"", expectedMessage, messages[i]) | ||
} | ||
} | ||
} | ||
|
||
func TestDestroy(t *testing.T) { | ||
a, err := NewStorageAdapter(10) | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
// Write a log to create the file | ||
if err := a.Write(app, "Hello, log!"); err != nil { | ||
t.Error(err) | ||
} | ||
// A redis list should exist for the app | ||
exists, err := a.redisClient.Exists(app).Result() | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
if !exists { | ||
t.Error("Log redis list was expected to exist, but doesn't.") | ||
} | ||
// Now destroy it | ||
if err := a.Destroy(app); err != nil { | ||
t.Error(err) | ||
} | ||
// Now check that the redis list no longer exists | ||
exists, err = a.redisClient.Exists(app).Result() | ||
if err != nil { | ||
t.Error(err) | ||
} | ||
if exists { | ||
t.Error("Log redis list still exist, but was expected not to.") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package redis | ||
|
||
import ( | ||
"github.com/kelseyhightower/envconfig" | ||
) | ||
|
||
const ( | ||
appName = "logger" | ||
) | ||
|
||
type config struct { | ||
RedisHost string `envconfig:"DEIS_LOGGER_REDIS_SERVICE_HOST" default:""` | ||
RedisPort int `envconfig:"DEIS_LOGGER_REDIS_SERVICE_PORT" default:"6379"` | ||
RedisPassword string `envconfig:"DEIS_LOGGER_REDIS_PASSWORD" default:""` | ||
RedisDB int `envconfig:"DEIS_LOGGER_REDIS_DB" default:"0"` | ||
} | ||
|
||
func parseConfig(appName string) (*config, error) { | ||
ret := new(config) | ||
if err := envconfig.Process(appName, ret); err != nil { | ||
return nil, err | ||
} | ||
return ret, nil | ||
} |