diff --git a/cmd/versitygw/main.go b/cmd/versitygw/main.go index ac60571b..2e735da2 100644 --- a/cmd/versitygw/main.go +++ b/cmd/versitygw/main.go @@ -45,8 +45,8 @@ var ( natsURL, natsTopic string eventWebhookURL string eventConfigFilePath string - logWebhookURL string - accessLog string + logWebhookURL, accessLog string + adminLogFile string healthPath string debug bool pprof string @@ -223,6 +223,12 @@ func initFlags() []cli.Flag { EnvVars: []string{"LOGFILE", "VGW_ACCESS_LOG"}, Destination: &accessLog, }, + &cli.StringFlag{ + Name: "admin-access-log", + Usage: "enable admin server access logging to specified file", + EnvVars: []string{"LOGFILE", "VGW_ADMIN_ACCESS_LOG"}, + Destination: &adminLogFile, + }, &cli.StringFlag{ Name: "log-webhook-url", Usage: "webhook url to send the audit logs", @@ -608,9 +614,10 @@ func runGateway(ctx context.Context, be backend.Backend) error { return fmt.Errorf("setup iam: %w", err) } - logger, err := s3log.InitLogger(&s3log.LogConfig{ - LogFile: accessLog, - WebhookURL: logWebhookURL, + loggers, err := s3log.InitLogger(&s3log.LogConfig{ + LogFile: accessLog, + WebhookURL: logWebhookURL, + AdminLogFile: adminLogFile, }) if err != nil { return fmt.Errorf("setup logger: %w", err) @@ -641,12 +648,12 @@ func runGateway(ctx context.Context, be backend.Backend) error { srv, err := s3api.New(app, be, middlewares.RootUserConfig{ Access: rootUserAccess, Secret: rootUserSecret, - }, port, region, iam, logger, evSender, metricsManager, opts...) + }, port, region, iam, loggers.S3Logger, loggers.AdminLogger, evSender, metricsManager, opts...) if err != nil { return fmt.Errorf("init gateway: %v", err) } - admSrv := s3api.NewAdminServer(admApp, be, middlewares.RootUserConfig{Access: rootUserAccess, Secret: rootUserSecret}, admPort, region, iam, admOpts...) + admSrv := s3api.NewAdminServer(admApp, be, middlewares.RootUserConfig{Access: rootUserAccess, Secret: rootUserSecret}, admPort, region, iam, loggers.AdminLogger, admOpts...) c := make(chan error, 2) go func() { c <- srv.Serve() }() @@ -663,10 +670,17 @@ Loop: case err = <-c: break Loop case <-sigHup: - if logger != nil { - err = logger.HangUp() + if loggers.S3Logger != nil { + err = loggers.S3Logger.HangUp() + if err != nil { + err = fmt.Errorf("HUP s3 logger: %w", err) + break Loop + } + } + if loggers.AdminLogger != nil { + err = loggers.AdminLogger.HangUp() if err != nil { - err = fmt.Errorf("HUP logger: %w", err) + err = fmt.Errorf("HUP admin logger: %w", err) break Loop } } @@ -684,13 +698,22 @@ Loop: fmt.Fprintf(os.Stderr, "shutdown iam: %v\n", err) } - if logger != nil { - err := logger.Shutdown() + if loggers.S3Logger != nil { + err := loggers.S3Logger.Shutdown() + if err != nil { + if saveErr == nil { + saveErr = err + } + fmt.Fprintf(os.Stderr, "shutdown s3 logger: %v\n", err) + } + } + if loggers.AdminLogger != nil { + err := loggers.AdminLogger.Shutdown() if err != nil { if saveErr == nil { saveErr = err } - fmt.Fprintf(os.Stderr, "shutdown logger: %v\n", err) + fmt.Fprintf(os.Stderr, "shutdown admin logger: %v\n", err) } } diff --git a/s3api/admin-router.go b/s3api/admin-router.go index 0abec3d4..65ec084a 100644 --- a/s3api/admin-router.go +++ b/s3api/admin-router.go @@ -19,12 +19,13 @@ import ( "github.com/versity/versitygw/auth" "github.com/versity/versitygw/backend" "github.com/versity/versitygw/s3api/controllers" + "github.com/versity/versitygw/s3log" ) type S3AdminRouter struct{} -func (ar *S3AdminRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMService) { - controller := controllers.NewAdminController(iam, be) +func (ar *S3AdminRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger) { + controller := controllers.NewAdminController(iam, be, logger) // CreateUser admin api app.Patch("/create-user", controller.CreateUser) diff --git a/s3api/admin-server.go b/s3api/admin-server.go index 1b45bd21..57290d35 100644 --- a/s3api/admin-server.go +++ b/s3api/admin-server.go @@ -22,6 +22,7 @@ import ( "github.com/versity/versitygw/auth" "github.com/versity/versitygw/backend" "github.com/versity/versitygw/s3api/middlewares" + "github.com/versity/versitygw/s3log" ) type S3AdminServer struct { @@ -32,7 +33,7 @@ type S3AdminServer struct { cert *tls.Certificate } -func NewAdminServer(app *fiber.App, be backend.Backend, root middlewares.RootUserConfig, port, region string, iam auth.IAMService, opts ...AdminOpt) *S3AdminServer { +func NewAdminServer(app *fiber.App, be backend.Backend, root middlewares.RootUserConfig, port, region string, iam auth.IAMService, l s3log.AuditLogger, opts ...AdminOpt) *S3AdminServer { server := &S3AdminServer{ app: app, backend: be, @@ -46,13 +47,13 @@ func NewAdminServer(app *fiber.App, be backend.Backend, root middlewares.RootUse // Logging middlewares app.Use(logger.New()) - app.Use(middlewares.DecodeURL(nil, nil)) + app.Use(middlewares.DecodeURL(l, nil)) // Authentication middlewares - app.Use(middlewares.VerifyV4Signature(root, iam, nil, nil, region, false)) - app.Use(middlewares.VerifyMD5Body(nil)) + app.Use(middlewares.VerifyV4Signature(root, iam, l, nil, region, false)) + app.Use(middlewares.VerifyMD5Body(l)) - server.router.Init(app, be, iam) + server.router.Init(app, be, iam, l) return server } diff --git a/s3api/controllers/admin.go b/s3api/controllers/admin.go index 1deaa039..bfd76642 100644 --- a/s3api/controllers/admin.go +++ b/s3api/controllers/admin.go @@ -16,6 +16,7 @@ package controllers import ( "encoding/json" + "errors" "fmt" "strings" @@ -23,120 +24,202 @@ import ( "github.com/gofiber/fiber/v2" "github.com/versity/versitygw/auth" "github.com/versity/versitygw/backend" + "github.com/versity/versitygw/s3log" ) type AdminController struct { iam auth.IAMService be backend.Backend + l s3log.AuditLogger } -func NewAdminController(iam auth.IAMService, be backend.Backend) AdminController { - return AdminController{iam: iam, be: be} +func NewAdminController(iam auth.IAMService, be backend.Backend, l s3log.AuditLogger) AdminController { + return AdminController{iam: iam, be: be, l: l} } func (c AdminController) CreateUser(ctx *fiber.Ctx) error { acct := ctx.Locals("account").(auth.Account) if acct.Role != "admin" { - return ctx.Status(fiber.StatusForbidden).SendString("access denied: only admin users have access to this resource") + return sendResponse(ctx, errors.New("access denied: only admin users have access to this resource"), nil, + &metaOptions{ + logger: c.l, + status: fiber.StatusForbidden, + action: "admin:CreateUser", + }) } var usr auth.Account err := json.Unmarshal(ctx.Body(), &usr) if err != nil { - return ctx.Status(fiber.StatusBadRequest).SendString(fmt.Errorf("failed to parse request body: %w", err).Error()) + return sendResponse(ctx, fmt.Errorf("failed to parse request body: %w", err), nil, + &metaOptions{ + logger: c.l, + status: fiber.StatusBadRequest, + action: "admin:CreateUser", + }) } if usr.Role != auth.RoleAdmin && usr.Role != auth.RoleUser && usr.Role != auth.RoleUserPlus { - return ctx.Status(fiber.StatusBadRequest).SendString("invalid parameters: user role have to be one of the following: 'user', 'admin', 'userplus'") + return sendResponse(ctx, errors.New("invalid parameters: user role have to be one of the following: 'user', 'admin', 'userplus'"), nil, + &metaOptions{ + logger: c.l, + status: fiber.StatusBadRequest, + action: "admin:CreateUser", + }) } err = c.iam.CreateAccount(usr) if err != nil { status := fiber.StatusInternalServerError - msg := fmt.Errorf("failed to create user: %w", err).Error() + err = fmt.Errorf("failed to create user: %w", err) - if strings.Contains(msg, "user already exists") { + if strings.Contains(err.Error(), "user already exists") { status = fiber.StatusConflict } - return ctx.Status(status).SendString(msg) + return sendResponse(ctx, err, nil, + &metaOptions{ + status: status, + logger: c.l, + action: "admin:CreateUser", + }) } - return ctx.Status(fiber.StatusCreated).SendString("The user has been created successfully") + return sendResponse(ctx, nil, "The user has been created successfully", &metaOptions{ + status: fiber.StatusCreated, + logger: c.l, + action: "admin:CreateUser", + }) } func (c AdminController) UpdateUser(ctx *fiber.Ctx) error { acct := ctx.Locals("account").(auth.Account) if acct.Role != "admin" { - return ctx.Status(fiber.StatusForbidden).SendString("access denied: only admin users have access to this resource") + return sendResponse(ctx, errors.New("access denied: only admin users have access to this resource"), nil, + &metaOptions{ + logger: c.l, + status: fiber.StatusForbidden, + action: "admin:UpdateUser", + }) } access := ctx.Query("access") if access == "" { - return ctx.Status(fiber.StatusBadRequest).SendString("missing user access parameter") + return sendResponse(ctx, errors.New("missing user access parameter"), nil, + &metaOptions{ + status: fiber.StatusBadRequest, + logger: c.l, + action: "admin:UpdateUser", + }) } var props auth.MutableProps if err := json.Unmarshal(ctx.Body(), &props); err != nil { - return ctx.Status(fiber.StatusBadRequest).SendString(fmt.Errorf("invalid request body %w", err).Error()) + return sendResponse(ctx, fmt.Errorf("invalid request body %w", err), nil, + &metaOptions{ + status: fiber.StatusBadRequest, + logger: c.l, + action: "admin:UpdateUser", + }) } err := c.iam.UpdateUserAccount(access, props) if err != nil { status := fiber.StatusInternalServerError - msg := fmt.Errorf("failed to update user account: %w", err).Error() + err = fmt.Errorf("failed to update user account: %w", err) - if strings.Contains(msg, "user not found") { + if strings.Contains(err.Error(), "user not found") { status = fiber.StatusNotFound } - return ctx.Status(status).SendString(msg) + return sendResponse(ctx, err, nil, + &metaOptions{ + status: status, + logger: c.l, + action: "admin:UpdateUser", + }) } - return ctx.SendString("the user has been updated successfully") + return sendResponse(ctx, nil, "the user has been updated successfully", + &metaOptions{ + logger: c.l, + action: "admin:UpdateUser", + }) } func (c AdminController) DeleteUser(ctx *fiber.Ctx) error { access := ctx.Query("access") acct := ctx.Locals("account").(auth.Account) if acct.Role != "admin" { - return ctx.Status(fiber.StatusForbidden).SendString("access denied: only admin users have access to this resource") + return sendResponse(ctx, errors.New("access denied: only admin users have access to this resource"), nil, + &metaOptions{ + logger: c.l, + status: fiber.StatusForbidden, + action: "admin:DeleteUser", + }) } err := c.iam.DeleteUserAccount(access) if err != nil { - return err + return sendResponse(ctx, err, nil, + &metaOptions{ + logger: c.l, + action: "admin:DeleteUser", + }) } - return ctx.SendString("The user has been deleted successfully") + return sendResponse(ctx, nil, "The user has been deleted successfully", + &metaOptions{ + logger: c.l, + action: "admin:DeleteUser", + }) } func (c AdminController) ListUsers(ctx *fiber.Ctx) error { acct := ctx.Locals("account").(auth.Account) if acct.Role != "admin" { - return ctx.Status(fiber.StatusForbidden).SendString("access denied: only admin users have access to this resource") + return sendResponse(ctx, errors.New("access denied: only admin users have access to this resource"), nil, + &metaOptions{ + logger: c.l, + status: fiber.StatusForbidden, + action: "admin:ListUsers", + }) } accs, err := c.iam.ListUserAccounts() - if err != nil { - return err - } - - return ctx.JSON(accs) + return sendResponse(ctx, err, accs, + &metaOptions{ + logger: c.l, + action: "admin:ListUsers", + }) } func (c AdminController) ChangeBucketOwner(ctx *fiber.Ctx) error { acct := ctx.Locals("account").(auth.Account) if acct.Role != "admin" { - return ctx.Status(fiber.StatusForbidden).SendString("access denied: only admin users have access to this resource") + return sendResponse(ctx, errors.New("access denied: only admin users have access to this resource"), nil, + &metaOptions{ + logger: c.l, + status: fiber.StatusForbidden, + action: "admin:ChangeBucketOwner", + }) } owner := ctx.Query("owner") bucket := ctx.Query("bucket") accs, err := auth.CheckIfAccountsExist([]string{owner}, c.iam) if err != nil { - return err + return sendResponse(ctx, err, nil, + &metaOptions{ + logger: c.l, + action: "admin:ChangeBucketOwner", + }) } if len(accs) > 0 { - return ctx.Status(fiber.StatusNotFound).SendString("user specified as the new bucket owner does not exist") + return sendResponse(ctx, errors.New("user specified as the new bucket owner does not exist"), nil, + &metaOptions{ + logger: c.l, + action: "admin:ChangeBucketOwner", + status: fiber.StatusNotFound, + }) } acl := auth.ACL{ @@ -151,27 +234,91 @@ func (c AdminController) ChangeBucketOwner(ctx *fiber.Ctx) error { aclParsed, err := json.Marshal(acl) if err != nil { - return fmt.Errorf("failed to marshal the bucket acl: %w", err) + return sendResponse(ctx, fmt.Errorf("failed to marshal the bucket acl: %w", err), nil, + &metaOptions{ + logger: c.l, + action: "admin:ChangeBucketOwner", + }) } err = c.be.ChangeBucketOwner(ctx.Context(), bucket, aclParsed) - if err != nil { - return err - } - - return ctx.SendString("Bucket owner has been updated successfully") + return sendResponse(ctx, err, "Bucket owner has been updated successfully", + &metaOptions{ + logger: c.l, + action: "admin:ChangeBucketOwner", + }) } func (c AdminController) ListBuckets(ctx *fiber.Ctx) error { acct := ctx.Locals("account").(auth.Account) if acct.Role != "admin" { - return ctx.Status(fiber.StatusForbidden).SendString("access denied: only admin users have access to this resource") + return sendResponse(ctx, errors.New("access denied: only admin users have access to this resource"), nil, + &metaOptions{ + logger: c.l, + status: fiber.StatusForbidden, + action: "admin:ListBuckets", + }) } buckets, err := c.be.ListBucketsAndOwners(ctx.Context()) + return sendResponse(ctx, err, buckets, + &metaOptions{ + logger: c.l, + action: "admin:ListBuckets", + }) +} + +type metaOptions struct { + action string + status int + logger s3log.AuditLogger +} + +func sendResponse(ctx *fiber.Ctx, err error, data any, m *metaOptions) error { + status := m.status + if err != nil { + if status == 0 { + status = fiber.StatusInternalServerError + } + if m.logger != nil { + m.logger.Log(ctx, err, []byte(err.Error()), s3log.LogMeta{ + Action: m.action, + HttpStatus: status, + }) + } + + return ctx.Status(status).SendString(err.Error()) + } + + if status == 0 { + status = fiber.StatusOK + } + + msg, ok := data.(string) + if ok { + if m.logger != nil { + m.logger.Log(ctx, nil, []byte(msg), s3log.LogMeta{ + Action: m.action, + HttpStatus: status, + }) + } + + return ctx.Status(status).SendString(msg) + } + + dataJSON, err := json.Marshal(data) if err != nil { return err } - return ctx.JSON(buckets) + if m.logger != nil { + m.logger.Log(ctx, nil, dataJSON, s3log.LogMeta{ + HttpStatus: status, + Action: m.action, + }) + } + + ctx.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSON) + + return ctx.Status(status).Send(dataJSON) } diff --git a/s3api/router.go b/s3api/router.go index c8d2279d..23b43d16 100644 --- a/s3api/router.go +++ b/s3api/router.go @@ -28,11 +28,11 @@ type S3ApiRouter struct { WithAdmSrv bool } -func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger, evs s3event.S3EventSender, mm *metrics.Manager, debug bool, readonly bool) { +func (sa *S3ApiRouter) Init(app *fiber.App, be backend.Backend, iam auth.IAMService, logger s3log.AuditLogger, aLogger s3log.AuditLogger, evs s3event.S3EventSender, mm *metrics.Manager, debug bool, readonly bool) { s3ApiController := controllers.New(be, iam, logger, evs, mm, debug, readonly) if sa.WithAdmSrv { - adminController := controllers.NewAdminController(iam, be) + adminController := controllers.NewAdminController(iam, be, aLogger) // CreateUser admin api app.Patch("/create-user", adminController.CreateUser) diff --git a/s3api/router_test.go b/s3api/router_test.go index 19b73728..58f01b31 100644 --- a/s3api/router_test.go +++ b/s3api/router_test.go @@ -45,7 +45,7 @@ func TestS3ApiRouter_Init(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tt.sa.Init(tt.args.app, tt.args.be, tt.args.iam, nil, nil, nil, false, false) + tt.sa.Init(tt.args.app, tt.args.be, tt.args.iam, nil, nil, nil, nil, false, false) }) } } diff --git a/s3api/server.go b/s3api/server.go index 1b1ef5e5..8bfebbc9 100644 --- a/s3api/server.go +++ b/s3api/server.go @@ -47,6 +47,7 @@ func New( port, region string, iam auth.IAMService, l s3log.AuditLogger, + adminLogger s3log.AuditLogger, evs s3event.S3EventSender, mm *metrics.Manager, opts ...Option, @@ -82,7 +83,7 @@ func New( app.Use(middlewares.VerifyMD5Body(l)) app.Use(middlewares.AclParser(be, l, server.readonly)) - server.router.Init(app, be, iam, l, evs, mm, server.debug, server.readonly) + server.router.Init(app, be, iam, l, adminLogger, evs, mm, server.debug, server.readonly) return server, nil } diff --git a/s3api/server_test.go b/s3api/server_test.go index aebf9dc0..64aa4fcd 100644 --- a/s3api/server_test.go +++ b/s3api/server_test.go @@ -64,7 +64,7 @@ func TestNew(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotS3ApiServer, err := New(tt.args.app, tt.args.be, tt.args.root, - tt.args.port, "us-east-1", &auth.IAMServiceInternal{}, nil, nil, nil) + tt.args.port, "us-east-1", &auth.IAMServiceInternal{}, nil, nil, nil, nil) if (err != nil) != tt.wantErr { t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/s3log/audit-logger.go b/s3log/audit-logger.go index 5f302663..24796a45 100644 --- a/s3log/audit-logger.go +++ b/s3log/audit-logger.go @@ -35,11 +35,13 @@ type LogMeta struct { BucketOwner string ObjectSize int64 Action string + HttpStatus int } type LogConfig struct { - LogFile string - WebhookURL string + LogFile string + WebhookURL string + AdminLogFile string } type LogFields struct { @@ -71,18 +73,66 @@ type LogFields struct { AclRequired string } -func InitLogger(cfg *LogConfig) (AuditLogger, error) { +type AdminLogFields struct { + Time time.Time + RemoteIP string + Requester string + RequestID string + Operation string + RequestURI string + HttpStatus int + ErrorCode string + BytesSent int + TotalTime int64 + TurnAroundTime int64 + Referer string + UserAgent string + SignatureVersion string + CipherSuite string + AuthenticationType string + TLSVersion string +} + +type Loggers struct { + S3Logger AuditLogger + AdminLogger AuditLogger +} + +func InitLogger(cfg *LogConfig) (*Loggers, error) { if cfg.WebhookURL != "" && cfg.LogFile != "" { return nil, fmt.Errorf("there should be specified one of the following: file, webhook") } - if cfg.WebhookURL != "" { - return InitWebhookLogger(cfg.WebhookURL) + loggers := new(Loggers) + + switch { + case cfg.WebhookURL != "": + fmt.Printf("initializing S3 access logs with '%v' webhook url\n", cfg.WebhookURL) + l, err := InitWebhookLogger(cfg.WebhookURL) + if err != nil { + return nil, err + } + loggers.S3Logger = l + case cfg.LogFile != "": + fmt.Printf("initializing S3 access logs with '%v' file\n", cfg.LogFile) + l, err := InitFileLogger(cfg.LogFile) + if err != nil { + return nil, err + } + + loggers.S3Logger = l } - if cfg.LogFile != "" { - return InitFileLogger(cfg.LogFile) + + if cfg.AdminLogFile != "" { + fmt.Printf("initializing admin access logs with '%v' file\n", cfg.AdminLogFile) + l, err := InitAdminFileLogger(cfg.AdminLogFile) + if err != nil { + return nil, err + } + + loggers.AdminLogger = l } - return nil, nil + return loggers, nil } func genID() string { diff --git a/s3log/file_admin.go b/s3log/file_admin.go new file mode 100644 index 00000000..73aa256a --- /dev/null +++ b/s3log/file_admin.go @@ -0,0 +1,151 @@ +// Copyright 2023 Versity Software +// This file is licensed under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package s3log + +import ( + "crypto/tls" + "fmt" + "os" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/versity/versitygw/auth" +) + +// FileLogger is a local file audit log +type AdminFileLogger struct { + FileLogger +} + +var _ AuditLogger = &AdminFileLogger{} + +// InitFileLogger initializes audit logs to local file +func InitAdminFileLogger(logname string) (AuditLogger, error) { + f, err := os.OpenFile(logname, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, fmt.Errorf("open log: %w", err) + } + + f.WriteString(fmt.Sprintf("log starts %v\n", time.Now())) + + return &AdminFileLogger{FileLogger: FileLogger{logfile: logname, f: f}}, nil +} + +// Log sends log message to file logger +func (f *AdminFileLogger) Log(ctx *fiber.Ctx, err error, body []byte, meta LogMeta) { + f.mu.Lock() + defer f.mu.Unlock() + + if f.gotErr { + return + } + + lf := AdminLogFields{} + + access := "-" + reqURI := ctx.OriginalURL() + errorCode := "" + startTime := ctx.Locals("startTime").(time.Time) + tlsConnState := ctx.Context().TLSConnectionState() + if tlsConnState != nil { + lf.CipherSuite = tls.CipherSuiteName(tlsConnState.CipherSuite) + lf.TLSVersion = getTLSVersionName(tlsConnState.Version) + } + + if err != nil { + errorCode = err.Error() + } + + switch ctx.Locals("account").(type) { + case auth.Account: + access = ctx.Locals("account").(auth.Account).Access + } + + lf.Time = time.Now() + lf.RemoteIP = ctx.IP() + lf.Requester = access + lf.RequestID = genID() + lf.Operation = meta.Action + lf.RequestURI = reqURI + lf.HttpStatus = meta.HttpStatus + lf.ErrorCode = errorCode + lf.BytesSent = len(body) + lf.TotalTime = time.Since(startTime).Milliseconds() + lf.TurnAroundTime = time.Since(startTime).Milliseconds() + lf.Referer = ctx.Get("Referer") + lf.UserAgent = ctx.Get("User-Agent") + lf.SignatureVersion = "SigV4" + lf.AuthenticationType = "AuthHeader" + + f.writeLog(lf) +} + +func (f *AdminFileLogger) writeLog(lf AdminLogFields) { + if lf.RemoteIP == "" { + lf.RemoteIP = "-" + } + if lf.Requester == "" { + lf.Requester = "-" + } + if lf.Operation == "" { + lf.Operation = "-" + } + if lf.RequestURI == "" { + lf.RequestURI = "-" + } + if lf.ErrorCode == "" { + lf.ErrorCode = "-" + } + if lf.Referer == "" { + lf.Referer = "-" + } + if lf.UserAgent == "" { + lf.UserAgent = "-" + } + if lf.CipherSuite == "" { + lf.CipherSuite = "-" + } + if lf.TLSVersion == "" { + lf.TLSVersion = "-" + } + + log := fmt.Sprintf("%v %v %v %v %v %v %v %v %v %v %v %v %v %v %v %v %v\n", + fmt.Sprintf("[%v]", lf.Time.Format(timeFormat)), + lf.RemoteIP, + lf.Requester, + lf.RequestID, + lf.Operation, + lf.RequestURI, + lf.HttpStatus, + lf.ErrorCode, + lf.BytesSent, + lf.TotalTime, + lf.TurnAroundTime, + lf.Referer, + lf.UserAgent, + lf.SignatureVersion, + lf.CipherSuite, + lf.AuthenticationType, + lf.TLSVersion, + ) + + _, err := f.f.WriteString(log) + if err != nil { + fmt.Fprintf(os.Stderr, "error writing to log file: %v\n", err) + // TODO: do we need to terminate on log error? + // set err for now so that we don't spew errors + f.gotErr = true + } +}