Skip to content

Commit 3f45fc5

Browse files
Merge pull request #260 from newrelic/cciutea/fix_storer_file_collision
feat: add unique path for storage file
2 parents 30ec2d6 + 2e12863 commit 3f45fc5

File tree

10 files changed

+298
-10
lines changed

10 files changed

+298
-10
lines changed

data/metric/metrics.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ func AddCustomAttributes(metricSet *Set, customAttributes []attribute.Attribute)
5959
}
6060
}
6161

62+
// AddNamespaceAttributes add attributes to MetricSet namespace.
63+
func (ms *Set) AddNamespaceAttributes(attributes ...attribute.Attribute) {
64+
for _, attr := range attributes {
65+
ms.nsAttributes = append(ms.nsAttributes, attr)
66+
}
67+
}
68+
6269
// SetMetric adds a metric to the Set object or updates the metric value if the metric already exists.
6370
// It calculates elapsed difference for RATE and DELTA types.
6471
func (ms *Set) SetMetric(name string, value interface{}, sourceType SourceType) (err error) {

data/metric/metrics_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,23 @@ func TestSet_SetMetricCachesRateAndDeltas(t *testing.T) {
9797
}
9898
}
9999

100+
func TestSet_SetMetricCache_NameSpaceSpecialChars(t *testing.T) {
101+
storer := persist.NewInMemoryStore()
102+
103+
ms := NewSet("some-event-type", storer, attribute.Attr("::==::::==", "::==::::"))
104+
err := ms.SetMetric("test", 3, DELTA)
105+
assert.NoError(t, err)
106+
107+
nameSpace := ms.namespace("test")
108+
109+
assert.Equal(t, "::==::::====::==::::::test", nameSpace)
110+
var v interface{}
111+
_, err = storer.Get(nameSpace, &v)
112+
assert.NoError(t, err)
113+
114+
assert.Equal(t, 3.0, v)
115+
}
116+
100117
func TestSet_SetMetricsRatesAndDeltas(t *testing.T) {
101118
var testCases = []struct {
102119
sourceType SourceType

integration/entity.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,14 @@ func (e *Entity) SameAs(b *Entity) bool {
111111

112112
// NewMetricSet returns a new instance of Set with its sample attached to the integration.
113113
func (e *Entity) NewMetricSet(eventType string, nameSpacingAttributes ...attribute.Attribute) *metric.Set {
114-
115114
s := metric.NewSet(eventType, e.storer, nameSpacingAttributes...)
116115

116+
if e.Metadata != nil {
117+
if key, err := e.Metadata.Key(); err == nil {
118+
s.AddNamespaceAttributes(attribute.Attr("entityKey", key.String()))
119+
}
120+
}
121+
117122
if len(e.customAttributes) > 0 {
118123
metric.AddCustomAttributes(s, e.customAttributes)
119124
}

integration/entity_id.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ func (a IDAttributes) Less(i, j int) bool {
8383

8484
func (a *IDAttributes) removeEmptyAndDuplicates() {
8585

86-
var uniques IDAttributes
86+
uniques := make(IDAttributes, 0)
87+
8788
var prev IDAttribute
8889
for i, attr := range *a {
8990
if prev.Key != attr.Key && attr.Key != "" {

integration/integration.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package integration
22

33
import (
44
"bytes"
5+
"crypto/md5"
56
"encoding/json"
67
"errors"
78
"fmt"
@@ -99,8 +100,14 @@ func New(name, version string, opts ...Option) (i *Integration, err error) {
99100
}
100101

101102
if i.storer == nil {
102-
var err error
103-
i.storer, err = persist.NewFileStore(persist.DefaultPath(i.Name), i.logger, persist.DefaultTTL)
103+
storePath, err := persist.NewStorePath(i.Name, i.CreateUniqueID(), i.logger, persist.DefaultTTL)
104+
if err != nil {
105+
return nil, fmt.Errorf("can't create temporary directory for store: %s", err)
106+
}
107+
108+
storePath.CleanOldFiles()
109+
110+
i.storer, err = persist.NewFileStore(storePath.GetFilePath(), i.logger, persist.DefaultTTL)
104111
if err != nil {
105112
return nil, fmt.Errorf("can't create store: %s", err)
106113
}
@@ -234,6 +241,14 @@ func (i *Integration) Logger() log.Logger {
234241
return i.logger
235242
}
236243

244+
// CreateUniqueID will generate an md5 string from integration arguments
245+
// to unique identify the integration instance.
246+
func (i *Integration) CreateUniqueID() string {
247+
h := md5.New()
248+
h.Write([]byte(fmt.Sprintf("%v", i.args)))
249+
return fmt.Sprintf("%x", h.Sum(nil))
250+
}
251+
237252
// toJSON serializes integration as JSON. If the pretty attribute is
238253
// set to true, the JSON will be indented for easy reading.
239254
func (i *Integration) toJSON(pretty bool) (output []byte, err error) {

integration/integration_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,38 @@ func TestIntegration_CreateLocalAndRemoteEntities(t *testing.T) {
452452
assert.NotEqual(t, remote, nil)
453453
}
454454

455+
func TestIntegration_CreateUniqueID_Default(t *testing.T) {
456+
type argumentList struct {
457+
sdk_args.DefaultArgumentList
458+
StatusURL string `default:"http://127.0.0.1/status" help:"NGINX status URL. If you are using ngx_http_api_module be sure to include the full path ending with the API version number"`
459+
}
460+
flag.CommandLine = flag.NewFlagSet("cmd", flag.ContinueOnError)
461+
462+
var al argumentList
463+
464+
i, err := New("testIntegration", "0.0.0", Args(&al))
465+
assert.NoError(t, err)
466+
467+
assert.Equal(t, i.CreateUniqueID(), "3071eb6863e28435e6c7e0c2bbe55ecd")
468+
}
469+
470+
func TestIntegration_CreateUniqueID_EnvironmentVar(t *testing.T) {
471+
type argumentList struct {
472+
sdk_args.DefaultArgumentList
473+
StatusURL string `default:"http://127.0.0.1/status" help:"NGINX status URL. If you are using ngx_http_api_module be sure to include the full path ending with the API version number"`
474+
}
475+
var al argumentList
476+
flag.CommandLine = flag.NewFlagSet("cmd", flag.ContinueOnError)
477+
478+
os.Setenv("STATUS_URL", "bar")
479+
defer os.Clearenv()
480+
481+
i, err := New("testIntegration", "0.0.0", Args(&al))
482+
assert.NoError(t, err)
483+
484+
assert.Equal(t, i.CreateUniqueID(), "2d998100982b7de9b4e446c85c3bed78")
485+
}
486+
455487
type testWriter struct {
456488
testFunc func([]byte)
457489
}

integration/options_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ func TestItStoresOnDiskByDefault(t *testing.T) {
7272
assert.NoError(t, i.Publish())
7373

7474
// assert data has been flushed to disk
75-
c, err := persist.NewFileStore(persist.DefaultPath(integrationName), log.NewStdErr(true), persist.DefaultTTL)
75+
storePath, err := persist.NewStorePath(i.Name, i.CreateUniqueID(), i.logger, persist.DefaultTTL)
76+
assert.NoError(t, err)
77+
78+
c, err := persist.NewFileStore(storePath.GetFilePath(), log.NewStdErr(true), persist.DefaultTTL)
7679
assert.NoError(t, err)
7780

7881
var v float64

persist/store_path.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package persist
2+
3+
import (
4+
"fmt"
5+
"github.com/newrelic/infra-integrations-sdk/log"
6+
"os"
7+
"path/filepath"
8+
"time"
9+
)
10+
11+
const (
12+
storeFileTemplate = "%s-%s.json"
13+
)
14+
15+
// StorePath will handle the location for the persistence.
16+
type StorePath interface {
17+
GetFilePath() string
18+
CleanOldFiles()
19+
}
20+
21+
// storePath will handle the location for the persistence.
22+
type storePath struct {
23+
dir string
24+
integrationName string
25+
integrationID string
26+
ilog log.Logger
27+
ttl time.Duration
28+
}
29+
30+
// NewStorePath create a new instance of StorePath
31+
func NewStorePath(integrationName, integrationID string, ilog log.Logger, ttl time.Duration) (StorePath, error) {
32+
if integrationName == "" {
33+
return nil, fmt.Errorf("integration name not specified")
34+
}
35+
36+
if integrationID == "" {
37+
return nil, fmt.Errorf("integration id not specified")
38+
}
39+
40+
if ttl == 0 {
41+
ttl = DefaultTTL
42+
}
43+
44+
return &storePath{
45+
dir: tmpIntegrationDir(),
46+
integrationName: integrationName,
47+
integrationID: integrationID,
48+
ilog: ilog,
49+
ttl: ttl,
50+
}, nil
51+
}
52+
53+
// GetFilePath will return the file for storing integration state.
54+
func (t *storePath) GetFilePath() string {
55+
return filepath.Join(t.dir, fmt.Sprintf(storeFileTemplate, t.integrationName, t.integrationID))
56+
}
57+
58+
// CleanOldFiles will remove all old files created by this integration.
59+
func (t *storePath) CleanOldFiles() {
60+
files, err := t.findOldFiles()
61+
if err != nil {
62+
t.ilog.Debugf("failed to cleanup old files: %v", err)
63+
return
64+
}
65+
66+
for _, file := range files {
67+
t.ilog.Debugf("removing store file (%s)", file)
68+
err := os.Remove(file)
69+
if err != nil {
70+
t.ilog.Debugf("failed to remove store file (%s): %v", file, err)
71+
continue
72+
}
73+
}
74+
}
75+
76+
// glob returns the pattern for finding all files for the same integration name.
77+
func (t *storePath) glob() string {
78+
return filepath.Join(t.dir, fmt.Sprintf(storeFileTemplate, t.integrationName, "*"))
79+
}
80+
81+
func (t *storePath) findOldFiles() ([]string, error) {
82+
var result []string
83+
// List all files by pattern: /tmp/nr-integrations/com.newrelic.nginx-*.json
84+
files, err := filepath.Glob(t.glob())
85+
if err != nil {
86+
return nil, err
87+
}
88+
for _, file := range files {
89+
if file == t.GetFilePath() {
90+
continue
91+
}
92+
93+
fileStat, err := os.Stat(file)
94+
if err != nil {
95+
continue
96+
}
97+
98+
if now().Sub(fileStat.ModTime()) > t.ttl {
99+
t.ilog.Debugf("store file (%s) is older than %v", fileStat.Name(), t.ttl)
100+
result = append(result, file)
101+
}
102+
}
103+
return result, nil
104+
}

persist/store_path_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package persist
2+
3+
import (
4+
"github.com/newrelic/infra-integrations-sdk/log"
5+
"github.com/stretchr/testify/assert"
6+
"os"
7+
"path/filepath"
8+
"testing"
9+
"time"
10+
)
11+
12+
var tmpDir string
13+
14+
func setupTestCase(t *testing.T) func(t *testing.T) {
15+
t.Log("setup test case")
16+
17+
assert.NoError(t, os.RemoveAll(filepath.Join(os.TempDir(), integrationsDir)))
18+
tmpDir = tmpIntegrationDir()
19+
20+
files := []struct {
21+
name string
22+
lastMod time.Duration
23+
}{
24+
{
25+
name: "com.newrelic.fake-a.json",
26+
lastMod: 1 * time.Second,
27+
},
28+
{
29+
name: "com.newrelic.fake-b.json",
30+
lastMod: 80 * time.Second,
31+
},
32+
{
33+
name: "com.newrelic.fake-c.json",
34+
lastMod: 80 * time.Second,
35+
},
36+
{
37+
name: "com.newrelic.flex-b.json",
38+
lastMod: 80 * time.Second,
39+
},
40+
}
41+
42+
for _, file := range files {
43+
f, err := os.Create(filepath.Join(tmpDir, file.name))
44+
assert.NoError(t, err)
45+
46+
lastChanged := time.Now().Local().Add(-file.lastMod)
47+
err = os.Chtimes(f.Name(), lastChanged, lastChanged)
48+
assert.NoError(t, err)
49+
}
50+
51+
return func(t *testing.T) {
52+
t.Log("teardown test case")
53+
assert.NoError(t, os.RemoveAll(tmpDir))
54+
}
55+
}
56+
57+
func TestStorePath_CleanOldFiles(t *testing.T) {
58+
59+
// GIVEN a tmp directory with multiple files
60+
tearDownFn := setupTestCase(t)
61+
defer tearDownFn(t)
62+
63+
// WHEN new store file is generated
64+
newPath, err := NewStorePath("com.newrelic.fake", "c", log.Discard, 1*time.Minute)
65+
assert.NoError(t, err)
66+
67+
// THEN only old files with different integration ID are removed
68+
newPath.CleanOldFiles()
69+
70+
files, err := filepath.Glob(filepath.Join(tmpDir, "*"))
71+
assert.NoError(t, err)
72+
73+
expected := []string{
74+
filepath.Join(tmpDir, "com.newrelic.fake-a.json"),
75+
filepath.Join(tmpDir, "com.newrelic.fake-c.json"),
76+
filepath.Join(tmpDir, "com.newrelic.flex-b.json"),
77+
}
78+
assert.Equal(t, expected, files)
79+
}
80+
81+
func TestStorePath_GetFilePath(t *testing.T) {
82+
storeFile, err := NewStorePath("com.newrelic.fake", "c", log.Discard, 1*time.Minute)
83+
assert.NoError(t, err)
84+
85+
expected := filepath.Join(tmpIntegrationDir(), "com.newrelic.fake-c.json")
86+
assert.Equal(t, expected, storeFile.GetFilePath())
87+
}
88+
89+
func TestStorePath_glob(t *testing.T) {
90+
storeFile, err := NewStorePath("com.newrelic.fake", "c", log.Discard, 1*time.Minute)
91+
assert.NoError(t, err)
92+
93+
tmp, ok := storeFile.(*storePath)
94+
assert.True(t, ok)
95+
96+
expected := filepath.Join(tmpIntegrationDir(), "com.newrelic.fake-*.json")
97+
assert.Equal(t, expected, tmp.glob())
98+
}

persist/storer.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,22 @@ func SetNow(newNow func() time.Time) {
7575
now = newNow
7676
}
7777

78-
// DefaultPath returns a default folder/filename path to a Storer for an integration from the given name. The name of
78+
// DefaultPath returns a default folder/filename dir to a Storer for an integration from the given name. The name of
7979
// the file will be the name of the integration with the .json extension.
8080
func DefaultPath(integrationName string) string {
81+
dir := tmpIntegrationDir()
82+
file := filepath.Join(dir, integrationName+".json")
83+
84+
return file
85+
}
86+
87+
func tmpIntegrationDir() string {
8188
dir := filepath.Join(os.TempDir(), integrationsDir)
82-
baseDir := path.Join(dir, integrationName+".json")
8389
// Create integrations Storer directory
8490
if os.MkdirAll(dir, dirFilePerm) != nil {
85-
baseDir = os.TempDir()
91+
dir = os.TempDir()
8692
}
87-
return baseDir
93+
return dir
8894
}
8995

9096
// NewInMemoryStore will create and initialize an in-memory Storer (not persistent).
@@ -96,7 +102,7 @@ func NewInMemoryStore() Storer {
96102
}
97103
}
98104

99-
// NewFileStore returns a disk-backed Storer using the provided file path
105+
// NewFileStore returns a disk-backed Storer using the provided file dir
100106
func NewFileStore(storagePath string, ilog log.Logger, ttl time.Duration) (Storer, error) {
101107
ms := NewInMemoryStore().(*inMemoryStore)
102108

0 commit comments

Comments
 (0)