|
- /*
- Copyright 2016 Google LLC
-
- 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 testing provides support for testing the logging client.
- package testing
-
- import (
- "errors"
- "fmt"
- "regexp"
- "sort"
- "strings"
- "sync"
- "time"
-
- emptypb "github.com/golang/protobuf/ptypes/empty"
- tspb "github.com/golang/protobuf/ptypes/timestamp"
-
- "cloud.google.com/go/internal/testutil"
- context "golang.org/x/net/context"
- lpb "google.golang.org/genproto/googleapis/api/label"
- mrpb "google.golang.org/genproto/googleapis/api/monitoredres"
- logpb "google.golang.org/genproto/googleapis/logging/v2"
- )
-
- type loggingHandler struct {
- logpb.LoggingServiceV2Server
-
- mu sync.Mutex
- logs map[string][]*logpb.LogEntry // indexed by log name
- }
-
- type configHandler struct {
- logpb.ConfigServiceV2Server
-
- mu sync.Mutex
- sinks map[string]*logpb.LogSink // indexed by (full) sink name
- }
-
- type metricHandler struct {
- logpb.MetricsServiceV2Server
-
- mu sync.Mutex
- metrics map[string]*logpb.LogMetric // indexed by (full) metric name
- }
-
- // NewServer creates a new in-memory fake server implementing the logging service.
- // It returns the address of the server.
- func NewServer() (string, error) {
- srv, err := testutil.NewServer()
- if err != nil {
- return "", err
- }
- logpb.RegisterLoggingServiceV2Server(srv.Gsrv, &loggingHandler{
- logs: make(map[string][]*logpb.LogEntry),
- })
- logpb.RegisterConfigServiceV2Server(srv.Gsrv, &configHandler{
- sinks: make(map[string]*logpb.LogSink),
- })
- logpb.RegisterMetricsServiceV2Server(srv.Gsrv, &metricHandler{
- metrics: make(map[string]*logpb.LogMetric),
- })
- srv.Start()
- return srv.Addr, nil
- }
-
- // DeleteLog deletes a log and all its log entries. The log will reappear if it
- // receives new entries.
- func (h *loggingHandler) DeleteLog(_ context.Context, req *logpb.DeleteLogRequest) (*emptypb.Empty, error) {
- // TODO(jba): return NotFound if log isn't there?
- h.mu.Lock()
- defer h.mu.Unlock()
- delete(h.logs, req.LogName)
- return &emptypb.Empty{}, nil
- }
-
- // The only IDs that WriteLogEntries will accept.
- // Important for testing Ping.
- const (
- validProjectID = "PROJECT_ID"
- validOrgID = "433637338589"
- )
-
- // WriteLogEntries writes log entries to Stackdriver Logging. All log entries in
- // Stackdriver Logging are written by this method.
- func (h *loggingHandler) WriteLogEntries(_ context.Context, req *logpb.WriteLogEntriesRequest) (*logpb.WriteLogEntriesResponse, error) {
- if !strings.HasPrefix(req.LogName, "projects/"+validProjectID+"/") && !strings.HasPrefix(req.LogName, "organizations/"+validOrgID+"/") {
- return nil, fmt.Errorf("bad LogName: %q", req.LogName)
- }
- // TODO(jba): support insertId?
- h.mu.Lock()
- defer h.mu.Unlock()
- for _, e := range req.Entries {
- // Assign timestamp if missing.
- if e.Timestamp == nil {
- e.Timestamp = &tspb.Timestamp{Seconds: time.Now().Unix(), Nanos: 0}
- }
- // Fill from common fields in request.
- if e.LogName == "" {
- e.LogName = req.LogName
- }
- if e.Resource == nil {
- // TODO(jba): use a global one if nil?
- e.Resource = req.Resource
- }
- for k, v := range req.Labels {
- if _, ok := e.Labels[k]; !ok {
- e.Labels[k] = v
- }
- }
-
- // Store by log name.
- h.logs[e.LogName] = append(h.logs[e.LogName], e)
- }
- return &logpb.WriteLogEntriesResponse{}, nil
- }
-
- // ListLogEntries lists log entries. Use this method to retrieve log entries
- // from Stackdriver Logging.
- //
- // This fake implementation ignores project IDs. It does not support full filtering, only
- // expressions of the form "logName = NAME".
- func (h *loggingHandler) ListLogEntries(_ context.Context, req *logpb.ListLogEntriesRequest) (*logpb.ListLogEntriesResponse, error) {
- h.mu.Lock()
- defer h.mu.Unlock()
- entries, err := h.filterEntries(req.Filter)
- if err != nil {
- return nil, err
- }
- if err = sortEntries(entries, req.OrderBy); err != nil {
- return nil, err
- }
-
- from, to, nextPageToken, err := testutil.PageBounds(int(req.PageSize), req.PageToken, len(entries))
- if err != nil {
- return nil, err
- }
- return &logpb.ListLogEntriesResponse{
- Entries: entries[from:to],
- NextPageToken: nextPageToken,
- }, nil
- }
-
- func (h *loggingHandler) filterEntries(filter string) ([]*logpb.LogEntry, error) {
- logName, err := parseFilter(filter)
- if err != nil {
- return nil, err
- }
- if logName != "" {
- return h.logs[logName], nil
- }
- var entries []*logpb.LogEntry
- for _, es := range h.logs {
- entries = append(entries, es...)
- }
- return entries, nil
- }
-
- var filterRegexp = regexp.MustCompile(`^logName\s*=\s*"?([-_/.%\w]+)"?$`)
-
- // returns the log name, or "" for the empty filter
- func parseFilter(filter string) (string, error) {
- if filter == "" {
- return "", nil
- }
- subs := filterRegexp.FindStringSubmatch(filter)
- if subs == nil {
- return "", invalidArgument("bad filter")
- }
- return subs[1], nil // cannot panic by construction of regexp
- }
-
- func sortEntries(entries []*logpb.LogEntry, orderBy string) error {
- switch orderBy {
- case "", "timestamp asc":
- sort.Sort(byTimestamp(entries))
- return nil
-
- case "timestamp desc":
- sort.Sort(sort.Reverse(byTimestamp(entries)))
- return nil
-
- default:
- return invalidArgument("bad order_by")
- }
- }
-
- type byTimestamp []*logpb.LogEntry
-
- func (s byTimestamp) Len() int { return len(s) }
- func (s byTimestamp) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
- func (s byTimestamp) Less(i, j int) bool {
- c := compareTimestamps(s[i].Timestamp, s[j].Timestamp)
- switch {
- case c < 0:
- return true
- case c > 0:
- return false
- default:
- return s[i].InsertId < s[j].InsertId
- }
- }
-
- func compareTimestamps(ts1, ts2 *tspb.Timestamp) int64 {
- if ts1.Seconds != ts2.Seconds {
- return ts1.Seconds - ts2.Seconds
- }
- return int64(ts1.Nanos - ts2.Nanos)
- }
-
- // Lists monitored resource descriptors that are used by Stackdriver Logging.
- func (h *loggingHandler) ListMonitoredResourceDescriptors(context.Context, *logpb.ListMonitoredResourceDescriptorsRequest) (*logpb.ListMonitoredResourceDescriptorsResponse, error) {
- return &logpb.ListMonitoredResourceDescriptorsResponse{
- ResourceDescriptors: []*mrpb.MonitoredResourceDescriptor{
- {
- Type: "global",
- DisplayName: "Global",
- Description: "... a log is not associated with any specific resource.",
- Labels: []*lpb.LabelDescriptor{
- {Key: "project_id", Description: "The identifier of the GCP project..."},
- },
- },
- },
- }, nil
- }
-
- // Lists logs.
- func (h *loggingHandler) ListLogs(_ context.Context, req *logpb.ListLogsRequest) (*logpb.ListLogsResponse, error) {
- // Return fixed, fake response.
- logNames := []string{"a", "b", "c"}
- from, to, npt, err := testutil.PageBounds(int(req.PageSize), req.PageToken, len(logNames))
- if err != nil {
- return nil, err
- }
- var lns []string
- for _, ln := range logNames[from:to] {
- lns = append(lns, req.Parent+"/logs/"+ln)
- }
- return &logpb.ListLogsResponse{
- LogNames: lns,
- NextPageToken: npt,
- }, nil
- }
-
- // Gets a sink.
- func (h *configHandler) GetSink(_ context.Context, req *logpb.GetSinkRequest) (*logpb.LogSink, error) {
- h.mu.Lock()
- defer h.mu.Unlock()
- if s, ok := h.sinks[req.SinkName]; ok {
- return s, nil
- }
- // TODO(jba): use error codes
- return nil, fmt.Errorf("sink %q not found", req.SinkName)
- }
-
- // Creates a sink.
- func (h *configHandler) CreateSink(_ context.Context, req *logpb.CreateSinkRequest) (*logpb.LogSink, error) {
- h.mu.Lock()
- defer h.mu.Unlock()
- fullName := fmt.Sprintf("%s/sinks/%s", req.Parent, req.Sink.Name)
- if _, ok := h.sinks[fullName]; ok {
- return nil, fmt.Errorf("sink with name %q already exists", fullName)
- }
- h.sinks[fullName] = req.Sink
- return req.Sink, nil
- }
-
- // Creates or updates a sink.
- func (h *configHandler) UpdateSink(_ context.Context, req *logpb.UpdateSinkRequest) (*logpb.LogSink, error) {
- h.mu.Lock()
- defer h.mu.Unlock()
- // Update of a non-existent sink will create it.
- h.sinks[req.SinkName] = req.Sink
- return req.Sink, nil
- }
-
- // Deletes a sink.
- func (h *configHandler) DeleteSink(_ context.Context, req *logpb.DeleteSinkRequest) (*emptypb.Empty, error) {
- h.mu.Lock()
- defer h.mu.Unlock()
- delete(h.sinks, req.SinkName)
- return &emptypb.Empty{}, nil
- }
-
- // Lists sinks. This fake implementation ignores the Parent field of
- // ListSinksRequest. All sinks are listed, regardless of their project.
- func (h *configHandler) ListSinks(_ context.Context, req *logpb.ListSinksRequest) (*logpb.ListSinksResponse, error) {
- h.mu.Lock()
- var sinks []*logpb.LogSink
- for _, s := range h.sinks {
- sinks = append(sinks, s)
- }
- h.mu.Unlock() // safe because no *logpb.LogSink is ever modified
- // Since map iteration varies, sort the sinks.
- sort.Sort(sinksByName(sinks))
- from, to, nextPageToken, err := testutil.PageBounds(int(req.PageSize), req.PageToken, len(sinks))
- if err != nil {
- return nil, err
- }
- return &logpb.ListSinksResponse{
- Sinks: sinks[from:to],
- NextPageToken: nextPageToken,
- }, nil
- }
-
- type sinksByName []*logpb.LogSink
-
- func (s sinksByName) Len() int { return len(s) }
- func (s sinksByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
- func (s sinksByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
-
- // Gets a metric.
- func (h *metricHandler) GetLogMetric(_ context.Context, req *logpb.GetLogMetricRequest) (*logpb.LogMetric, error) {
- h.mu.Lock()
- defer h.mu.Unlock()
- if s, ok := h.metrics[req.MetricName]; ok {
- return s, nil
- }
- // TODO(jba): use error codes
- return nil, fmt.Errorf("metric %q not found", req.MetricName)
- }
-
- // Creates a metric.
- func (h *metricHandler) CreateLogMetric(_ context.Context, req *logpb.CreateLogMetricRequest) (*logpb.LogMetric, error) {
- h.mu.Lock()
- defer h.mu.Unlock()
- fullName := fmt.Sprintf("%s/metrics/%s", req.Parent, req.Metric.Name)
- if _, ok := h.metrics[fullName]; ok {
- return nil, fmt.Errorf("metric with name %q already exists", fullName)
- }
- h.metrics[fullName] = req.Metric
- return req.Metric, nil
- }
-
- // Creates or updates a metric.
- func (h *metricHandler) UpdateLogMetric(_ context.Context, req *logpb.UpdateLogMetricRequest) (*logpb.LogMetric, error) {
- h.mu.Lock()
- defer h.mu.Unlock()
- // Update of a non-existent metric will create it.
- h.metrics[req.MetricName] = req.Metric
- return req.Metric, nil
- }
-
- // Deletes a metric.
- func (h *metricHandler) DeleteLogMetric(_ context.Context, req *logpb.DeleteLogMetricRequest) (*emptypb.Empty, error) {
- h.mu.Lock()
- defer h.mu.Unlock()
- delete(h.metrics, req.MetricName)
- return &emptypb.Empty{}, nil
- }
-
- // Lists metrics. This fake implementation ignores the Parent field of
- // ListMetricsRequest. All metrics are listed, regardless of their project.
- func (h *metricHandler) ListLogMetrics(_ context.Context, req *logpb.ListLogMetricsRequest) (*logpb.ListLogMetricsResponse, error) {
- h.mu.Lock()
- var metrics []*logpb.LogMetric
- for _, s := range h.metrics {
- metrics = append(metrics, s)
- }
- h.mu.Unlock() // safe because no *logpb.LogMetric is ever modified
- // Since map iteration varies, sort the metrics.
- sort.Sort(metricsByName(metrics))
- from, to, nextPageToken, err := testutil.PageBounds(int(req.PageSize), req.PageToken, len(metrics))
- if err != nil {
- return nil, err
- }
- return &logpb.ListLogMetricsResponse{
- Metrics: metrics[from:to],
- NextPageToken: nextPageToken,
- }, nil
- }
-
- type metricsByName []*logpb.LogMetric
-
- func (s metricsByName) Len() int { return len(s) }
- func (s metricsByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
- func (s metricsByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
-
- func invalidArgument(msg string) error {
- // TODO(jba): status codes
- return errors.New(msg)
- }
|