Added structure to data manager log entries, grouping similar fields.
[arvados.git] / sdk / go / logger / logger.go
1 // Logger periodically writes a log to the Arvados SDK.
2 //
3 // This package is useful for maintaining a log object that is built
4 // up over time. Every time the object is modified, it will be written
5 // to the log. Writes will be throttled to no more than one every
6 // WriteFrequencySeconds
7 //
8 // This package is safe for concurrent use as long as:
9 // 1. The maps returned by Edit() are only edited in the same routine
10 //    that called Edit()
11 // 2. Those maps not edited after calling Record()
12 // An easy way to assure this is true is to place the call to Edit()
13 // within a short block as shown below in the Usage Example:
14 //
15 // Usage:
16 // arvLogger := logger.NewLogger(params)
17 // {
18 //   properties, entry := arvLogger.Edit()  // This will block if others are using the logger
19 //   // Modifiy properties and entry however you want
20 //   // properties is a shortcut for entry["properties"].(map[string]interface{})
21 //   // properties can take any values you want to give it,
22 //   // entry will only take the fields listed at http://doc.arvados.org/api/schema/Log.html
23 // }
24 // arvLogger.Record()  // This triggers the actual log write
25 package logger
26
27 import (
28         "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
29         "log"
30         "sync"
31         "time"
32 )
33
34 type LoggerParams struct {
35         Client arvadosclient.ArvadosClient  // The client we use to write log entries
36         EventType string  // The event type to assign to the log entry.
37         MinimumWriteInterval time.Duration  // Wait at least this long between log writes
38 }
39
40 // A Logger is used to build up a log entry over time and write every
41 // version of it.
42 type Logger struct {
43         // The Data we write
44         data        map[string]interface{}  // The entire map that we give to the api
45         entry       map[string]interface{}  // Convenience shortcut into data
46         properties  map[string]interface{}  // Convenience shortcut into data
47
48         lock        sync.Locker   // Synchronizes editing and writing
49         params      LoggerParams  // Parameters we were given
50
51         lastWrite   time.Time  // The last time we wrote a log entry
52         modified    bool       // Has this data been modified since the last write
53 }
54
55 // Create a new logger based on the specified parameters.
56 func NewLogger(params LoggerParams) *Logger {
57         // TODO(misha): Add some params checking here.
58         l := &Logger{data: make(map[string]interface{}),
59                 lock: &sync.Mutex{},
60                 params: params}
61         l.entry = make(map[string]interface{})
62         l.data["log"] = l.entry
63         l.properties = make(map[string]interface{})
64         l.entry["properties"] = l.properties
65         return l
66 }
67
68 // Get access to the maps you can edit. This will hold a lock until
69 // you call Record. Do not edit the maps in any other goroutines or
70 // after calling Record.
71 // You don't need to edit both maps, 
72 // properties can take any values you want to give it,
73 // entry will only take the fields listed at http://doc.arvados.org/api/schema/Log.html
74 // properties is a shortcut for entry["properties"].(map[string]interface{})
75 func (l *Logger) Edit() (properties map[string]interface{}, entry map[string]interface{}) {
76         l.lock.Lock()
77         l.modified = true  // We don't actually know the caller will modifiy the data, but we assume they will.
78         return l.properties, l.entry
79 }
80
81 // Write the log entry you've built up so far. Do not edit the maps
82 // returned by Edit() after calling this method.
83 // If you have already written within MinimumWriteInterval, then this
84 // will schedule a future write instead.
85 // In either case, the lock will be released before Record() returns.
86 func (l *Logger) Record() {
87         if l.writeAllowedNow() {
88                 // We haven't written in the allowed interval yet, try to write.
89                 l.write()
90         } else {
91                 nextTimeToWrite := l.lastWrite.Add(l.params.MinimumWriteInterval)
92                 writeAfter := nextTimeToWrite.Sub(time.Now())
93                 time.AfterFunc(writeAfter, l.acquireLockConsiderWriting)
94         }
95         l.lock.Unlock()
96 }
97
98
99 // Whether enough time has elapsed since the last write.
100 func (l *Logger) writeAllowedNow() bool {
101         return l.lastWrite.Add(l.params.MinimumWriteInterval).Before(time.Now())
102 }
103
104
105 // Actually writes the log entry. This method assumes we're holding the lock.
106 func (l *Logger) write() {
107         // Update the event type in case it was modified or is missing.
108         l.entry["event_type"] = l.params.EventType
109         err := l.params.Client.Create("logs", l.data, nil)
110         if err != nil {
111                 log.Printf("Attempted to log: %v", l.data)
112                 log.Fatalf("Received error writing log: %v", err)
113         }
114         l.lastWrite = time.Now()
115         l.modified = false
116 }
117
118
119 func (l *Logger) acquireLockConsiderWriting() {
120         l.lock.Lock()
121         if l.modified && l.writeAllowedNow() {
122                 // We have something new to write and we're allowed to write.
123                 l.write()
124         }
125         l.lock.Unlock()
126 }