1 // Logger periodically writes a log to the Arvados SDK.
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
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
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:
16 // arvLogger := logger.NewLogger(params)
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
24 // arvLogger.Record() // This triggers the actual log write
28 "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
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
40 // A Logger is used to build up a log entry over time and write every
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
48 lock sync.Locker // Synchronizes editing and writing
49 params LoggerParams // Parameters we were given
51 lastWrite time.Time // The last time we wrote a log entry
52 modified bool // Has this data been modified since the last write
54 writeHooks []func(map[string]interface{},map[string]interface{})
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{}),
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
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{}) {
79 l.modified = true // We don't actually know the caller will modifiy the data, but we assume they will.
81 return l.properties, l.entry
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)
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.
103 nextTimeToWrite := l.lastWrite.Add(l.params.MinimumWriteInterval)
104 writeAfter := nextTimeToWrite.Sub(time.Now())
105 time.AfterFunc(writeAfter, l.acquireLockConsiderWriting)
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())
117 // Actually writes the log entry. This method assumes we're holding the lock.
118 func (l *Logger) write() {
121 for _, hook := range l.writeHooks {
122 hook(l.properties, l.entry)
125 // Update the event type in case it was modified or is missing.
126 l.entry["event_type"] = l.params.EventType
128 // Write the log entry.
129 err := l.params.Client.Create("logs", l.data, nil)
131 log.Printf("Attempted to log: %v", l.data)
132 log.Fatalf("Received error writing log: %v", err)
136 l.lastWrite = time.Now()
141 func (l *Logger) acquireLockConsiderWriting() {
143 if l.modified && l.writeAllowedNow() {
144 // We have something new to write and we're allowed to write.