github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/core/auditlog/auditlog.go (about) 1 // Copyright 2017 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package auditlog 5 6 import ( 7 "encoding/hex" 8 "encoding/json" 9 "fmt" 10 "io" 11 "math/rand" 12 "path/filepath" 13 "time" 14 15 "github.com/juju/clock" 16 "github.com/juju/errors" 17 "github.com/juju/loggo" 18 "github.com/juju/lumberjack/v2" 19 20 "github.com/juju/juju/core/paths" 21 ) 22 23 var logger = loggo.GetLogger("core.auditlog") 24 25 // Conversation represents a high-level juju command from the juju 26 // client (or other client). There'll be one Conversation per API 27 // connection from the client, with zero or more associated 28 // Request/ResponseErrors pairs. 29 type Conversation struct { 30 Who string `json:"who"` // username@idm 31 What string `json:"what"` // "juju deploy ./foo/bar" 32 When string `json:"when"` // ISO 8601 to second precision 33 ModelName string `json:"model-name"` // full representation "user/name" 34 ModelUUID string `json:"model-uuid"` 35 ConversationID string `json:"conversation-id"` // uint64 in hex 36 ConnectionID string `json:"connection-id"` // uint64 in hex (using %X to match the value in log files) 37 } 38 39 // ConversationArgs is the information needed to create a method recorder. 40 type ConversationArgs struct { 41 Who string 42 What string 43 ModelName string 44 ModelUUID string 45 ConnectionID uint64 46 } 47 48 // Request represents a call to an API facade made as part of 49 // a specific conversation. 50 type Request struct { 51 ConversationID string `json:"conversation-id"` 52 ConnectionID string `json:"connection-id"` 53 RequestID uint64 `json:"request-id"` 54 When string `json:"when"` 55 Facade string `json:"facade"` 56 Method string `json:"method"` 57 Version int `json:"version"` 58 Args string `json:"args,omitempty"` 59 } 60 61 // RequestArgs is the information about an API call that we want to 62 // record. 63 type RequestArgs struct { 64 Facade string 65 Method string 66 Version int 67 Args string 68 RequestID uint64 69 } 70 71 // ResponseErrors captures any errors coming back from the API in 72 // response to a request. 73 type ResponseErrors struct { 74 ConversationID string `json:"conversation-id"` 75 ConnectionID string `json:"connection-id"` 76 RequestID uint64 `json:"request-id"` 77 When string `json:"when"` 78 Errors []*Error `json:"errors"` 79 } 80 81 // ResponseErrorsArgs has errors from an API response to record in the 82 // audit log. 83 type ResponseErrorsArgs struct { 84 RequestID uint64 85 Errors []*Error 86 } 87 88 // Error holds the details of an error sent back from the API. 89 type Error struct { 90 Message string `json:"message"` 91 Code string `json:"code"` 92 } 93 94 // Record is the top-level entry type in an audit log, which serves as 95 // a type discriminator. Only one of Conversation/Request/Errors should be set. 96 type Record struct { 97 Conversation *Conversation `json:"conversation,omitempty"` 98 Request *Request `json:"request,omitempty"` 99 Errors *ResponseErrors `json:"errors,omitempty"` 100 } 101 102 // AuditLog represents something that can store calls, requests and 103 // responses somewhere. 104 type AuditLog interface { 105 AddConversation(c Conversation) error 106 AddRequest(r Request) error 107 AddResponse(r ResponseErrors) error 108 Close() error 109 } 110 111 // Recorder records method calls for a specific API connection. 112 type Recorder struct { 113 log AuditLog 114 clock clock.Clock 115 connectionID string 116 callID string 117 } 118 119 // NewRecorder creates a Recorder for the connection described (and 120 // stores details of the initial call in the log). 121 func NewRecorder(log AuditLog, clock clock.Clock, c ConversationArgs) (*Recorder, error) { 122 callID := newConversationID() 123 connectionID := idString(c.ConnectionID) 124 err := log.AddConversation(Conversation{ 125 ConversationID: callID, 126 ConnectionID: connectionID, 127 Who: c.Who, 128 What: c.What, 129 When: clock.Now().Format(time.RFC3339), 130 ModelName: c.ModelName, 131 ModelUUID: c.ModelUUID, 132 }) 133 if err != nil { 134 return nil, errors.Trace(err) 135 } 136 return &Recorder{ 137 log: log, 138 clock: clock, 139 callID: callID, 140 connectionID: connectionID, 141 }, nil 142 } 143 144 // AddRequest records a method call to the API. 145 func (r *Recorder) AddRequest(m RequestArgs) error { 146 return errors.Trace(r.log.AddRequest(Request{ 147 ConversationID: r.callID, 148 ConnectionID: r.connectionID, 149 RequestID: m.RequestID, 150 When: r.clock.Now().Format(time.RFC3339), 151 Facade: m.Facade, 152 Method: m.Method, 153 Version: m.Version, 154 Args: m.Args, 155 })) 156 } 157 158 // AddResponse records the result of a method call to the API. 159 func (r *Recorder) AddResponse(m ResponseErrorsArgs) error { 160 return errors.Trace(r.log.AddResponse(ResponseErrors{ 161 ConversationID: r.callID, 162 ConnectionID: r.connectionID, 163 RequestID: m.RequestID, 164 When: r.clock.Now().Format(time.RFC3339), 165 Errors: m.Errors, 166 })) 167 } 168 169 // newConversationID generates a random 64bit integer as hex - this 170 // will be used to link the requests and responses with the command 171 // the user issued. We don't use the API server's connection ID here 172 // because that starts from 0 and increments, so it resets when the 173 // API server is restarted. The conversation ID needs to be unique 174 // across restarts, otherwise we'd attribute requests to the wrong 175 // conversation. 176 func newConversationID() string { 177 buf := make([]byte, 8) 178 rand.Read(buf) // Can't fail 179 return hex.EncodeToString(buf) 180 } 181 182 type auditLogFile struct { 183 fileLogger io.WriteCloser 184 } 185 186 // NewLogFile returns an audit entry sink which writes to an audit.log 187 // file in the specified directory. maxSize is the maximum size (in 188 // megabytes) of the log file before it gets rotated. maxBackups is 189 // the maximum number of old compressed log files to keep (or 0 to 190 // keep all of them). 191 func NewLogFile(logDir string, maxSize, maxBackups int) AuditLog { 192 logPath := filepath.Join(logDir, "audit.log") 193 if err := paths.PrimeLogFile(logPath); err != nil { 194 // This isn't a fatal error so log and continue if priming 195 // fails. 196 logger.Errorf("Unable to prime %s (proceeding anyway): %v", logPath, err) 197 } 198 199 ljLogger := &lumberjack.Logger{ 200 Filename: logPath, 201 MaxSize: maxSize, 202 MaxBackups: maxBackups, 203 Compress: true, 204 } 205 logger.Debugf("created rotating log file %q with max size %d MB and max backups %d", 206 ljLogger.Filename, ljLogger.MaxSize, ljLogger.MaxBackups) 207 return &auditLogFile{ 208 fileLogger: ljLogger, 209 } 210 } 211 212 // AddConversation implements AuditLog. 213 func (a *auditLogFile) AddConversation(c Conversation) error { 214 return errors.Trace(a.addRecord(Record{Conversation: &c})) 215 } 216 217 // AddRequest implements AuditLog. 218 func (a *auditLogFile) AddRequest(m Request) error { 219 return errors.Trace(a.addRecord(Record{Request: &m})) 220 221 } 222 223 // AddResponse implements AuditLog. 224 func (a *auditLogFile) AddResponse(m ResponseErrors) error { 225 return errors.Trace(a.addRecord(Record{Errors: &m})) 226 } 227 228 // Close implements AuditLog. 229 func (a *auditLogFile) Close() error { 230 return errors.Trace(a.fileLogger.Close()) 231 } 232 233 func (a *auditLogFile) addRecord(r Record) error { 234 bytes, err := json.Marshal(r) 235 if err != nil { 236 return errors.Trace(err) 237 } 238 // Add a linebreak to bytes rather than doing two calls to write 239 // just in case lumberjack rolls the file between them. 240 bytes = append(bytes, byte('\n')) 241 _, err = a.fileLogger.Write(bytes) 242 return errors.Trace(err) 243 } 244 245 func idString(id uint64) string { 246 return fmt.Sprintf("%X", id) 247 }