diff --git a/cmd/logs.go b/cmd/logs.go index fd5e2cd26..22f6ab585 100644 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -18,6 +18,7 @@ func newLogsCmd() *cobra.Command { var stack string var follow bool var since string + var resource string logsCmd := &cobra.Command{ Use: "logs", @@ -29,6 +30,11 @@ func newLogsCmd() *cobra.Command { } startTime := parseRelativeDuration(since) + var resourceFilter *operations.ResourceFilter + if resource != "" { + var rf = operations.ResourceFilter(resource) + resourceFilter = &rf + } // IDEA: This map will grow forever as new log entries are found. We may need to do a more approximate // approach here to ensure we don't grow memory unboundedly while following logs. @@ -41,6 +47,7 @@ func newLogsCmd() *cobra.Command { for { logs, err := backend.GetLogs(stackName, operations.LogQuery{ StartTime: startTime, + Resource: resourceFilter, }) if err != nil { return err @@ -72,6 +79,9 @@ func newLogsCmd() *cobra.Command { logsCmd.PersistentFlags().StringVar( &since, "since", "", "Only return logs newer than a relative duration ('5s', '2m', '3h'). Defaults to returning all logs.") + logsCmd.PersistentFlags().StringVarP( + &resource, "resource", "r", "", + "Only return logs for the requested resource ('name', 'type::name' or full URN). Defaults to returning all logs.") return logsCmd } diff --git a/pkg/operations/operations.go b/pkg/operations/operations.go index c31ae299f..246220791 100644 --- a/pkg/operations/operations.go +++ b/pkg/operations/operations.go @@ -11,12 +11,31 @@ type LogEntry struct { Message string } +// ResourceFilter specifies a specific resource or subset of resources. It can be provided in three formats: +// - Full URN: "::::::" +// - Type + Name: "::" +// - Name: "" +type ResourceFilter string + +// Query is a filter on logs, in the format '=,=>val>' where is one of the following: +// - `URN` +// - `QualifiedName` +// - `Name` +// - `Type` +// - `Message` +type Query string + // LogQuery represents the parameters to a log query operation. // All fields are optional, leaving them off returns all logs. type LogQuery struct { + // StartTime is an optional time indiciating that only logs from after this time should be produced. StartTime *time.Time - EndTime *time.Time - Query *string + // EndTime is an optional time indiciating that only logs from before this time should be produced. + EndTime *time.Time + // Query is a string indicating a filter to apply to the logs - query syntax TBD + Query *string + // Resource is a string indicating that logs should be limited toa resource of resoruces + Resource *ResourceFilter } // MetricName is a handle to a metric supported by a Pulumi Framework resources diff --git a/pkg/operations/resources.go b/pkg/operations/resources.go index cc519c992..09aa1a6e3 100644 --- a/pkg/operations/resources.go +++ b/pkg/operations/resources.go @@ -87,29 +87,34 @@ var _ Provider = (*resourceOperations)(nil) // GetLogs gets logs for a Resource func (ops *resourceOperations) GetLogs(query LogQuery) (*[]LogEntry, error) { - opsProvider, err := ops.getOperationsProvider() - if err != nil { - return nil, err - } - if opsProvider != nil { - // If this resource has an operations provider - use it and don't recur into children. It is the responsibility - // of it's GetLogs implementation to aggregate all logs from children, either by passing them through or by - // filtering specific content out. - logsResult, err := opsProvider.GetLogs(query) + // Only get logs for this resource if it matches the resource filter query + if ops.matchesResourceFilter(query.Resource) { + // Try to get an operations provider for this resource, it may be `nil` + opsProvider, err := ops.getOperationsProvider() if err != nil { - return logsResult, err + return nil, err } - if logsResult != nil { - return logsResult, nil + if opsProvider != nil { + // If this resource has an operations provider - use it and don't recur into children. It is the + // responsibility of it's GetLogs implementation to aggregate all logs from children, either by passing them + // through or by filtering specific content out. + logsResult, err := opsProvider.GetLogs(query) + if err != nil { + return logsResult, err + } + if logsResult != nil { + return logsResult, nil + } } } + // If this resource did not choose to provide it's own logs, recur into children and collect + aggregate their logs. var logs []LogEntry for _, child := range ops.resource.children { childOps := &resourceOperations{ resource: child, config: ops.config, } - // TODO: Parallelize these calls to child GetLogs + // IDEA: Parallelize these calls to child GetLogs childLogs, err := childOps.GetLogs(query) if err != nil { return &logs, err @@ -146,6 +151,31 @@ func (ops *resourceOperations) GetLogs(query LogQuery) (*[]LogEntry, error) { return &retLogs, nil } +// matchesResourceFilter determines whether this resource matches the provided resource filter. +func (ops *resourceOperations) matchesResourceFilter(filter *ResourceFilter) bool { + if filter == nil { + // No filter, all resources match it. + return true + } + if ops.resource == nil || ops.resource.state == nil { + return false + } + urn := ops.resource.state.URN + if resource.URN(*filter) == urn { + // The filter matched the full URN + return true + } + if string(*filter) == string(urn.Type())+"::"+string(urn.Name()) { + // The filter matched the '::' part of the URN + return true + } + if tokens.QName(*filter) == urn.Name() { + // The filter matched the '' part of the URN + return true + } + return false +} + // ListMetrics lists metrics for a Resource func (ops *resourceOperations) ListMetrics() []MetricName { return []MetricName{}