| // 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 ( |
| "errors" |
| "fmt" |
| "strings" |
| "time" |
| |
| "github.com/golang/protobuf/proto" |
| "golang.org/x/net/context" |
| |
| "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) |
| } |