038482dda885704d9b9bb09d209913530c96b57a
[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         writeHooks  []func(map[string]interface{},map[string]interface{})
55 }
56
57 // Create a new logger based on the specified parameters.
58 func NewLogger(params LoggerParams) *Logger {
59         // TODO(misha): Add some params checking here.
60         l := &Logger{data: make(map[string]interface{}),
61                 lock: &sync.Mutex{},
62                 params: params}
63         l.entry = make(map[string]interface{})
64         l.data["log"] = l.entry
65         l.properties = make(map[string]interface{})
66         l.entry["properties"] = l.properties
67         return l
68 }
69
70 // Get access to the maps you can edit. This will hold a lock until
71 // you call Record. Do not edit the maps in any other goroutines or
72 // after calling Record.
73 // You don't need to edit both maps, 
74 // properties can take any values you want to give it,
75 // entry will only take the fields listed at http://doc.arvados.org/api/schema/Log.html
76 // properties is a shortcut for entry["properties"].(map[string]interface{})
77 func (l *Logger) Edit() (properties map[string]interface{}, entry map[string]interface{}) {
78         l.lock.Lock()
79         l.modified = true  // We don't actually know the caller will modifiy the data, but we assume they will.
80
81         return l.properties, l.entry
82 }
83
84 // Adds a hook which will be called every time this logger writes an entry.
85 // The hook takes properties and entry as arguments, in that order.
86 // This is useful for stuff like memory profiling.
87 // This must be called between Edit() and Record() (e.g. while holding the lock)
88 func (l *Logger) AddWriteHook(hook func(map[string]interface{},
89         map[string]interface{})) {
90         l.writeHooks = append(l.writeHooks, hook)
91 }
92
93 // Write the log entry you've built up so far. Do not edit the maps
94 // returned by Edit() after calling this method.
95 // If you have already written within MinimumWriteInterval, then this
96 // will schedule a future write instead.
97 // In either case, the lock will be released before Record() returns.
98 func (l *Logger) Record() {
99         if l.writeAllowedNow() {
100                 // We haven't written in the allowed interval yet, try to write.
101                 l.write()
102         } else {
103                 nextTimeToWrite := l.lastWrite.Add(l.params.MinimumWriteInterval)
104                 writeAfter := nextTimeToWrite.Sub(time.Now())
105                 time.AfterFunc(writeAfter, l.acquireLockConsiderWriting)
106         }
107         l.lock.Unlock()
108 }
109
110
111 // Whether enough time has elapsed since the last write.
112 func (l *Logger) writeAllowedNow() bool {
113         return l.lastWrite.Add(l.params.MinimumWriteInterval).Before(time.Now())
114 }
115
116
117 // Actually writes the log entry. This method assumes we're holding the lock.
118 func (l *Logger) write() {
119
120         // Run all our hooks
121         for _, hook := range l.writeHooks {
122                 hook(l.properties, l.entry)
123         }
124
125         // Update the event type in case it was modified or is missing.
126         l.entry["event_type"] = l.params.EventType
127
128         // Write the log entry.
129         err := l.params.Client.Create("logs", l.data, nil)
130         if err != nil {
131                 log.Printf("Attempted to log: %v", l.data)
132                 log.Fatalf("Received error writing log: %v", err)
133         }
134
135         // Update stats.
136         l.lastWrite = time.Now()
137         l.modified = false
138 }
139
140
141 func (l *Logger) acquireLockConsiderWriting() {
142         l.lock.Lock()
143         if l.modified && l.writeAllowedNow() {
144                 // We have something new to write and we're allowed to write.
145                 l.write()
146         }
147         l.lock.Unlock()
148 }