| // Copyright 2011 Google Inc. All rights reserved. | |
| // Use of this source code is governed by the Apache 2.0 | |
| // license that can be found in the LICENSE file. | |
| /* | |
| Package log provides the means of writing and querying an application's logs | |
| from within an App Engine application. | |
| Example: | |
| c := appengine.NewContext(r) | |
| query := &log.Query{ | |
| AppLogs: true, | |
| Versions: []string{"1"}, | |
| } | |
| for results := query.Run(c); ; { | |
| record, err := results.Next() | |
| if err == log.Done { | |
| log.Infof(c, "Done processing results") | |
| break | |
| } | |
| if err != nil { | |
| log.Errorf(c, "Failed to retrieve next log: %v", err) | |
| break | |
| } | |
| log.Infof(c, "Saw record %v", record) | |
| } | |
| */ | |
| package log // import "google.golang.org/appengine/log" | |
| import ( | |
| "context" | |
| "errors" | |
| "fmt" | |
| "strings" | |
| "time" | |
| "github.com/golang/protobuf/proto" | |
| "google.golang.org/appengine" | |
| "google.golang.org/appengine/internal" | |
| pb "google.golang.org/appengine/internal/log" | |
| ) | |
| // Query defines a logs query. | |
| type Query struct { | |
| // Start time specifies the earliest log to return (inclusive). | |
| StartTime time.Time | |
| // End time specifies the latest log to return (exclusive). | |
| EndTime time.Time | |
| // Offset specifies a position within the log stream to resume reading from, | |
| // and should come from a previously returned Record's field of the same name. | |
| Offset []byte | |
| // Incomplete controls whether active (incomplete) requests should be included. | |
| Incomplete bool | |
| // AppLogs indicates if application-level logs should be included. | |
| AppLogs bool | |
| // ApplyMinLevel indicates if MinLevel should be used to filter results. | |
| ApplyMinLevel bool | |
| // If ApplyMinLevel is true, only logs for requests with at least one | |
| // application log of MinLevel or higher will be returned. | |
| MinLevel int | |
| // Versions is the major version IDs whose logs should be retrieved. | |
| // Logs for specific modules can be retrieved by the specifying versions | |
| // in the form "module:version"; the default module is used if no module | |
| // is specified. | |
| Versions []string | |
| // A list of requests to search for instead of a time-based scan. Cannot be | |
| // combined with filtering options such as StartTime, EndTime, Offset, | |
| // Incomplete, ApplyMinLevel, or Versions. | |
| RequestIDs []string | |
| } | |
| // AppLog represents a single application-level log. | |
| type AppLog struct { | |
| Time time.Time | |
| Level int | |
| Message string | |
| } | |
| // Record contains all the information for a single web request. | |
| type Record struct { | |
| AppID string | |
| ModuleID string | |
| VersionID string | |
| RequestID []byte | |
| IP string | |
| Nickname string | |
| AppEngineRelease string | |
| // The time when this request started. | |
| StartTime time.Time | |
| // The time when this request finished. | |
| EndTime time.Time | |
| // Opaque cursor into the result stream. | |
| Offset []byte | |
| // The time required to process the request. | |
| Latency time.Duration | |
| MCycles int64 | |
| Method string | |
| Resource string | |
| HTTPVersion string | |
| Status int32 | |
| // The size of the request sent back to the client, in bytes. | |
| ResponseSize int64 | |
| Referrer string | |
| UserAgent string | |
| URLMapEntry string | |
| Combined string | |
| Host string | |
| // The estimated cost of this request, in dollars. | |
| Cost float64 | |
| TaskQueueName string | |
| TaskName string | |
| WasLoadingRequest bool | |
| PendingTime time.Duration | |
| Finished bool | |
| AppLogs []AppLog | |
| // Mostly-unique identifier for the instance that handled the request if available. | |
| InstanceID string | |
| } | |
| // Result represents the result of a query. | |
| type Result struct { | |
| logs []*Record | |
| context context.Context | |
| request *pb.LogReadRequest | |
| resultsSeen bool | |
| err error | |
| } | |
| // Next returns the next log record, | |
| func (qr *Result) Next() (*Record, error) { | |
| if qr.err != nil { | |
| return nil, qr.err | |
| } | |
| if len(qr.logs) > 0 { | |
| lr := qr.logs[0] | |
| qr.logs = qr.logs[1:] | |
| return lr, nil | |
| } | |
| if qr.request.Offset == nil && qr.resultsSeen { | |
| return nil, Done | |
| } | |
| if err := qr.run(); err != nil { | |
| // Errors here may be retried, so don't store the error. | |
| return nil, err | |
| } | |
| return qr.Next() | |
| } | |
| // Done is returned when a query iteration has completed. | |
| var Done = errors.New("log: query has no more results") | |
| // protoToAppLogs takes as input an array of pointers to LogLines, the internal | |
| // Protocol Buffer representation of a single application-level log, | |
| // and converts it to an array of AppLogs, the external representation | |
| // of an application-level log. | |
| func protoToAppLogs(logLines []*pb.LogLine) []AppLog { | |
| appLogs := make([]AppLog, len(logLines)) | |
| for i, line := range logLines { | |
| appLogs[i] = AppLog{ | |
| Time: time.Unix(0, *line.Time*1e3), | |
| Level: int(*line.Level), | |
| Message: *line.LogMessage, | |
| } | |
| } | |
| return appLogs | |
| } | |
| // protoToRecord converts a RequestLog, the internal Protocol Buffer | |
| // representation of a single request-level log, to a Record, its | |
| // corresponding external representation. | |
| func protoToRecord(rl *pb.RequestLog) *Record { | |
| offset, err := proto.Marshal(rl.Offset) | |
| if err != nil { | |
| offset = nil | |
| } | |
| return &Record{ | |
| AppID: *rl.AppId, | |
| ModuleID: rl.GetModuleId(), | |
| VersionID: *rl.VersionId, | |
| RequestID: rl.RequestId, | |
| Offset: offset, | |
| IP: *rl.Ip, | |
| Nickname: rl.GetNickname(), | |
| AppEngineRelease: string(rl.GetAppEngineRelease()), | |
| StartTime: time.Unix(0, *rl.StartTime*1e3), | |
| EndTime: time.Unix(0, *rl.EndTime*1e3), | |
| Latency: time.Duration(*rl.Latency) * time.Microsecond, | |
| MCycles: *rl.Mcycles, | |
| Method: *rl.Method, | |
| Resource: *rl.Resource, | |
| HTTPVersion: *rl.HttpVersion, | |
| Status: *rl.Status, | |
| ResponseSize: *rl.ResponseSize, | |
| Referrer: rl.GetReferrer(), | |
| UserAgent: rl.GetUserAgent(), | |
| URLMapEntry: *rl.UrlMapEntry, | |
| Combined: *rl.Combined, | |
| Host: rl.GetHost(), | |
| Cost: rl.GetCost(), | |
| TaskQueueName: rl.GetTaskQueueName(), | |
| TaskName: rl.GetTaskName(), | |
| WasLoadingRequest: rl.GetWasLoadingRequest(), | |
| PendingTime: time.Duration(rl.GetPendingTime()) * time.Microsecond, | |
| Finished: rl.GetFinished(), | |
| AppLogs: protoToAppLogs(rl.Line), | |
| InstanceID: string(rl.GetCloneKey()), | |
| } | |
| } | |
| // Run starts a query for log records, which contain request and application | |
| // level log information. | |
| func (params *Query) Run(c context.Context) *Result { | |
| req, err := makeRequest(params, internal.FullyQualifiedAppID(c), appengine.VersionID(c)) | |
| return &Result{ | |
| context: c, | |
| request: req, | |
| err: err, | |
| } | |
| } | |
| func makeRequest(params *Query, appID, versionID string) (*pb.LogReadRequest, error) { | |
| req := &pb.LogReadRequest{} | |
| req.AppId = &appID | |
| if !params.StartTime.IsZero() { | |
| req.StartTime = proto.Int64(params.StartTime.UnixNano() / 1e3) | |
| } | |
| if !params.EndTime.IsZero() { | |
| req.EndTime = proto.Int64(params.EndTime.UnixNano() / 1e3) | |
| } | |
| if len(params.Offset) > 0 { | |
| var offset pb.LogOffset | |
| if err := proto.Unmarshal(params.Offset, &offset); err != nil { | |
| return nil, fmt.Errorf("bad Offset: %v", err) | |
| } | |
| req.Offset = &offset | |
| } | |
| if params.Incomplete { | |
| req.IncludeIncomplete = ¶ms.Incomplete | |
| } | |
| if params.AppLogs { | |
| req.IncludeAppLogs = ¶ms.AppLogs | |
| } | |
| if params.ApplyMinLevel { | |
| req.MinimumLogLevel = proto.Int32(int32(params.MinLevel)) | |
| } | |
| if params.Versions == nil { | |
| // If no versions were specified, default to the default module at | |
| // the major version being used by this module. | |
| if i := strings.Index(versionID, "."); i >= 0 { | |
| versionID = versionID[:i] | |
| } | |
| req.VersionId = []string{versionID} | |
| } else { | |
| req.ModuleVersion = make([]*pb.LogModuleVersion, 0, len(params.Versions)) | |
| for _, v := range params.Versions { | |
| var m *string | |
| if i := strings.Index(v, ":"); i >= 0 { | |
| m, v = proto.String(v[:i]), v[i+1:] | |
| } | |
| req.ModuleVersion = append(req.ModuleVersion, &pb.LogModuleVersion{ | |
| ModuleId: m, | |
| VersionId: proto.String(v), | |
| }) | |
| } | |
| } | |
| if params.RequestIDs != nil { | |
| ids := make([][]byte, len(params.RequestIDs)) | |
| for i, v := range params.RequestIDs { | |
| ids[i] = []byte(v) | |
| } | |
| req.RequestId = ids | |
| } | |
| return req, nil | |
| } | |
| // run takes the query Result produced by a call to Run and updates it with | |
| // more Records. The updated Result contains a new set of logs as well as an | |
| // offset to where more logs can be found. We also convert the items in the | |
| // response from their internal representations to external versions of the | |
| // same structs. | |
| func (r *Result) run() error { | |
| res := &pb.LogReadResponse{} | |
| if err := internal.Call(r.context, "logservice", "Read", r.request, res); err != nil { | |
| return err | |
| } | |
| r.logs = make([]*Record, len(res.Log)) | |
| r.request.Offset = res.Offset | |
| r.resultsSeen = true | |
| for i, log := range res.Log { | |
| r.logs[i] = protoToRecord(log) | |
| } | |
| return nil | |
| } | |
| func init() { | |
| internal.RegisterErrorCodeMap("logservice", pb.LogServiceError_ErrorCode_name) | |
| } |