1 // Logger periodically writes a log to the Arvados SDK.
3 // This package is useful for maintaining a log object that is updated
4 // over time. Every time the object is updated, it will be written to
5 // the log. Writes will be throttled to no more frequent than
8 // This package is safe for concurrent use as long as:
9 // The maps passed to a LogMutator are not accessed outside of the
13 // arvLogger := logger.NewLogger(params)
14 // arvLogger.Update(func(properties map[string]interface{},
15 // entry map[string]interface{}) {
16 // // Modifiy properties and entry however you want
17 // // properties is a shortcut for entry["properties"].(map[string]interface{})
18 // // properties can take any values you want to give it,
19 // // entry will only take the fields listed at http://doc.arvados.org/api/schema/Log.html
24 "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
29 type LoggerParams struct {
30 Client arvadosclient.ArvadosClient // The client we use to write log entries
31 EventType string // The event type to assign to the log entry.
32 WriteInterval time.Duration // Wait at least this long between log writes
35 // A LogMutator is a function which modifies the log entry.
36 // It takes two maps as arguments, properties is the first and entry
38 // properties is a shortcut for entry["properties"].(map[string]interface{})
39 // properties can take any values you want to give it.
40 // entry will only take the fields listed at http://doc.arvados.org/api/schema/Log.html
41 // properties and entry are only safe to access inside the LogMutator,
42 // they should not be stored anywhere, otherwise you'll risk
44 type LogMutator func(map[string]interface{}, map[string]interface{})
46 // A Logger is used to build up a log entry over time and write every
50 data map[string]interface{} // The entire map that we give to the api
51 entry map[string]interface{} // Convenience shortcut into data
52 properties map[string]interface{} // Convenience shortcut into data
54 params LoggerParams // Parameters we were given
56 // Variables to coordinate updating and writing.
57 modified bool // Has this data been modified since the last write?
58 workToDo chan LogMutator // Work to do in the worker thread.
59 writeTicker *time.Ticker // On each tick we write the log data to arvados, if it has been modified.
61 writeHooks []LogMutator // Mutators we call before each write.
64 // Create a new logger based on the specified parameters.
65 func NewLogger(params LoggerParams) *Logger {
66 // sanity check parameters
67 if ¶ms.Client == nil {
68 log.Fatal("Nil arvados client in LoggerParams passed in to NewLogger()")
70 if params.EventType == "" {
71 log.Fatal("Empty event type in LoggerParams passed in to NewLogger()")
74 l := &Logger{data: make(map[string]interface{}),
76 l.entry = make(map[string]interface{})
77 l.data["log"] = l.entry
78 l.properties = make(map[string]interface{})
79 l.entry["properties"] = l.properties
81 l.workToDo = make(chan LogMutator, 10)
82 l.writeTicker = time.NewTicker(params.WriteInterval)
84 // Start the worker goroutine.
90 // Exported functions will be called from other goroutines, therefore
91 // all they are allowed to do is enqueue work to be done in the worker
94 // Enqueues an update. This will happen in another goroutine after
95 // this method returns.
96 func (l *Logger) Update(mutator LogMutator) {
100 // Similar to Update(), but writes the log entry as soon as possible
101 // (ignoring MinimumWriteInterval) and blocks until the entry has been
102 // written. This is useful if you know that you're about to quit
103 // (e.g. if you discovered a fatal error, or you're finished), since
104 // go will not wait for timers (including the pending write timer) to
105 // go off before exiting.
106 func (l *Logger) FinalUpdate(mutator LogMutator) {
107 // Block on this channel until everything finishes
108 done := make(chan bool)
110 // TODO(misha): Consider not accepting any future updates somehow,
111 // since they won't get written if they come in after this.
113 // Stop the periodic write ticker. We'll perform the final write
114 // before returning from this function.
115 l.workToDo <- func(p map[string]interface{}, e map[string]interface{}) {
119 // Apply the final update
120 l.workToDo <- mutator
122 // Perform the write and signal that we can return.
123 l.workToDo <- func(p map[string]interface{}, e map[string]interface{}) {
124 // TODO(misha): Add a boolean arg to write() to indicate that it's
125 // final so that we can set the appropriate event type.
130 // Wait until we've performed the write.
134 // Adds a hook which will be called every time this logger writes an entry.
135 func (l *Logger) AddWriteHook(hook LogMutator) {
136 // We do the work in a LogMutator so that it happens in the worker
138 l.workToDo <- func(p map[string]interface{}, e map[string]interface{}) {
139 l.writeHooks = append(l.writeHooks, hook)
144 func (l *Logger) work() {
147 case <-l.writeTicker.C:
152 case mutator := <-l.workToDo:
153 mutator(l.properties, l.entry)
159 // Actually writes the log entry.
160 func (l *Logger) write() {
163 for _, hook := range l.writeHooks {
164 hook(l.properties, l.entry)
167 // Update the event type in case it was modified or is missing.
168 // TODO(misha): Fix this to write different event types.
169 l.entry["event_type"] = l.params.EventType
171 // Write the log entry.
172 // This is a network write and will take a while, which is bad
173 // because we're blocking all the other work on this goroutine.
175 // TODO(misha): Consider rewriting this so that we can encode l.data
176 // into a string, and then perform the actual write in another
177 // routine. This will be tricky and will require support in the
179 err := l.params.Client.Create("logs", l.data, nil)
181 log.Printf("Attempted to log: %v", l.data)
182 log.Fatalf("Received error writing log: %v", err)