...

Source file src/cloud.google.com/go/logging/internal/testing/fake.go

Documentation: cloud.google.com/go/logging/internal/testing

     1  /*
     2  Copyright 2016 Google LLC
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package testing provides support for testing the logging client.
    18  package testing
    19  
    20  import (
    21  	"context"
    22  	"errors"
    23  	"fmt"
    24  	"regexp"
    25  	"sort"
    26  	"strings"
    27  	"sync"
    28  	"time"
    29  
    30  	"cloud.google.com/go/internal/testutil"
    31  	logpb "cloud.google.com/go/logging/apiv2/loggingpb"
    32  	lpb "google.golang.org/genproto/googleapis/api/label"
    33  	mrpb "google.golang.org/genproto/googleapis/api/monitoredres"
    34  	emptypb "google.golang.org/protobuf/types/known/emptypb"
    35  	tspb "google.golang.org/protobuf/types/known/timestamppb"
    36  )
    37  
    38  type loggingHandler struct {
    39  	logpb.LoggingServiceV2Server
    40  
    41  	mu   sync.Mutex
    42  	logs map[string][]*logpb.LogEntry // indexed by log name
    43  }
    44  
    45  type configHandler struct {
    46  	logpb.ConfigServiceV2Server
    47  
    48  	mu    sync.Mutex
    49  	sinks map[string]*logpb.LogSink // indexed by (full) sink name
    50  }
    51  
    52  type metricHandler struct {
    53  	logpb.MetricsServiceV2Server
    54  
    55  	mu      sync.Mutex
    56  	metrics map[string]*logpb.LogMetric // indexed by (full) metric name
    57  }
    58  
    59  // NewServer creates a new in-memory fake server implementing the logging service.
    60  // It returns the address of the server.
    61  func NewServer() (string, error) {
    62  	srv, err := testutil.NewServer()
    63  	if err != nil {
    64  		return "", err
    65  	}
    66  	logpb.RegisterLoggingServiceV2Server(srv.Gsrv, &loggingHandler{
    67  		logs: make(map[string][]*logpb.LogEntry),
    68  	})
    69  	logpb.RegisterConfigServiceV2Server(srv.Gsrv, &configHandler{
    70  		sinks: make(map[string]*logpb.LogSink),
    71  	})
    72  	logpb.RegisterMetricsServiceV2Server(srv.Gsrv, &metricHandler{
    73  		metrics: make(map[string]*logpb.LogMetric),
    74  	})
    75  	srv.Start()
    76  	return srv.Addr, nil
    77  }
    78  
    79  // DeleteLog deletes a log and all its log entries. The log will reappear if it
    80  // receives new entries.
    81  func (h *loggingHandler) DeleteLog(_ context.Context, req *logpb.DeleteLogRequest) (*emptypb.Empty, error) {
    82  	// TODO(jba): return NotFound if log isn't there?
    83  	h.mu.Lock()
    84  	defer h.mu.Unlock()
    85  	delete(h.logs, req.LogName)
    86  	return &emptypb.Empty{}, nil
    87  }
    88  
    89  // The only IDs that WriteLogEntries will accept.
    90  // Important for testing Ping.
    91  const (
    92  	ValidProjectID = "PROJECT_ID"
    93  	ValidOrgID     = "433637338589"
    94  
    95  	SharedServiceAccount = "serviceAccount:cloud-logs@system.gserviceaccount.com"
    96  )
    97  
    98  // WriteLogEntries writes log entries to Cloud Logging. All log entries in
    99  // Cloud Logging are written by this method.
   100  func (h *loggingHandler) WriteLogEntries(_ context.Context, req *logpb.WriteLogEntriesRequest) (*logpb.WriteLogEntriesResponse, error) {
   101  	if !strings.HasPrefix(req.LogName, "projects/"+ValidProjectID+"/") && !strings.HasPrefix(req.LogName, "organizations/"+ValidOrgID+"/") {
   102  		return nil, fmt.Errorf("bad LogName: %q", req.LogName)
   103  	}
   104  	// TODO(jba): support insertId?
   105  	h.mu.Lock()
   106  	defer h.mu.Unlock()
   107  	for _, e := range req.Entries {
   108  		// Assign timestamp if missing.
   109  		if e.Timestamp == nil {
   110  			e.Timestamp = &tspb.Timestamp{Seconds: time.Now().Unix(), Nanos: 0}
   111  		}
   112  		// Fill from common fields in request.
   113  		if e.LogName == "" {
   114  			e.LogName = req.LogName
   115  		}
   116  		if e.Resource == nil {
   117  			// TODO(jba): use a global one if nil?
   118  			e.Resource = req.Resource
   119  		}
   120  		for k, v := range req.Labels {
   121  			if _, ok := e.Labels[k]; !ok {
   122  				e.Labels[k] = v
   123  			}
   124  		}
   125  
   126  		// Store by log name.
   127  		h.logs[e.LogName] = append(h.logs[e.LogName], e)
   128  	}
   129  	return &logpb.WriteLogEntriesResponse{}, nil
   130  }
   131  
   132  // ListLogEntries lists log entries. Use this method to retrieve log entries
   133  // from Cloud Logging.
   134  //
   135  // This fake implementation ignores project IDs. It does not support full filtering, only
   136  // expressions of the form "logName = NAME".
   137  func (h *loggingHandler) ListLogEntries(_ context.Context, req *logpb.ListLogEntriesRequest) (*logpb.ListLogEntriesResponse, error) {
   138  	h.mu.Lock()
   139  	defer h.mu.Unlock()
   140  	entries, err := h.filterEntries(req.Filter)
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  	if err = sortEntries(entries, req.OrderBy); err != nil {
   145  		return nil, err
   146  	}
   147  
   148  	from, to, nextPageToken, err := testutil.PageBounds(int(req.PageSize), req.PageToken, len(entries))
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  	return &logpb.ListLogEntriesResponse{
   153  		Entries:       entries[from:to],
   154  		NextPageToken: nextPageToken,
   155  	}, nil
   156  }
   157  
   158  func (h *loggingHandler) filterEntries(filter string) ([]*logpb.LogEntry, error) {
   159  	logName, err := parseFilter(filter)
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  	if logName != "" {
   164  		return h.logs[logName], nil
   165  	}
   166  	var entries []*logpb.LogEntry
   167  	for _, es := range h.logs {
   168  		entries = append(entries, es...)
   169  	}
   170  	return entries, nil
   171  }
   172  
   173  var filterRegexp = regexp.MustCompile(`^logName\s*=\s*"?([-_/.%\w]+)"?`)
   174  
   175  // returns the log name, or "" for the empty filter
   176  func parseFilter(filter string) (string, error) {
   177  	if filter == "" {
   178  		return "", nil
   179  	}
   180  	subs := filterRegexp.FindStringSubmatch(filter)
   181  	if subs == nil {
   182  		return "", invalidArgument(fmt.Sprintf("fake.go: failed to parse filter %s", filter))
   183  	}
   184  	return subs[1], nil // cannot panic by construction of regexp
   185  }
   186  
   187  func sortEntries(entries []*logpb.LogEntry, orderBy string) error {
   188  	switch orderBy {
   189  	case "", "timestamp asc":
   190  		sort.Sort(byTimestamp(entries))
   191  		return nil
   192  
   193  	case "timestamp desc":
   194  		sort.Sort(sort.Reverse(byTimestamp(entries)))
   195  		return nil
   196  
   197  	default:
   198  		return invalidArgument("bad order_by")
   199  	}
   200  }
   201  
   202  type byTimestamp []*logpb.LogEntry
   203  
   204  func (s byTimestamp) Len() int      { return len(s) }
   205  func (s byTimestamp) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
   206  func (s byTimestamp) Less(i, j int) bool {
   207  	c := compareTimestamps(s[i].Timestamp, s[j].Timestamp)
   208  	switch {
   209  	case c < 0:
   210  		return true
   211  	case c > 0:
   212  		return false
   213  	default:
   214  		return s[i].InsertId < s[j].InsertId
   215  	}
   216  }
   217  
   218  func compareTimestamps(ts1, ts2 *tspb.Timestamp) int64 {
   219  	if ts1.Seconds != ts2.Seconds {
   220  		return ts1.Seconds - ts2.Seconds
   221  	}
   222  	return int64(ts1.Nanos - ts2.Nanos)
   223  }
   224  
   225  // Lists monitored resource descriptors that are used by Cloud Logging.
   226  func (h *loggingHandler) ListMonitoredResourceDescriptors(context.Context, *logpb.ListMonitoredResourceDescriptorsRequest) (*logpb.ListMonitoredResourceDescriptorsResponse, error) {
   227  	return &logpb.ListMonitoredResourceDescriptorsResponse{
   228  		ResourceDescriptors: []*mrpb.MonitoredResourceDescriptor{
   229  			{
   230  				Type:        "global",
   231  				DisplayName: "Global",
   232  				Description: "... a log is not associated with any specific resource.",
   233  				Labels: []*lpb.LabelDescriptor{
   234  					{Key: "project_id", Description: "The identifier of the GCP project..."},
   235  				},
   236  			},
   237  		},
   238  	}, nil
   239  }
   240  
   241  // Lists logs.
   242  func (h *loggingHandler) ListLogs(_ context.Context, req *logpb.ListLogsRequest) (*logpb.ListLogsResponse, error) {
   243  	// Return fixed, fake response.
   244  	logNames := []string{"a", "b", "c"}
   245  	from, to, npt, err := testutil.PageBounds(int(req.PageSize), req.PageToken, len(logNames))
   246  	if err != nil {
   247  		return nil, err
   248  	}
   249  	var lns []string
   250  	for _, ln := range logNames[from:to] {
   251  		lns = append(lns, req.Parent+"/logs/"+ln)
   252  	}
   253  	return &logpb.ListLogsResponse{
   254  		LogNames:      lns,
   255  		NextPageToken: npt,
   256  	}, nil
   257  }
   258  
   259  // Gets a sink.
   260  func (h *configHandler) GetSink(_ context.Context, req *logpb.GetSinkRequest) (*logpb.LogSink, error) {
   261  	h.mu.Lock()
   262  	defer h.mu.Unlock()
   263  	if s, ok := h.sinks[req.SinkName]; ok {
   264  		return s, nil
   265  	}
   266  	// TODO(jba): use error codes
   267  	return nil, fmt.Errorf("sink %q not found", req.SinkName)
   268  }
   269  
   270  // Creates a sink.
   271  func (h *configHandler) CreateSink(_ context.Context, req *logpb.CreateSinkRequest) (*logpb.LogSink, error) {
   272  	h.mu.Lock()
   273  	defer h.mu.Unlock()
   274  	fullName := fmt.Sprintf("%s/sinks/%s", req.Parent, req.Sink.Name)
   275  	if _, ok := h.sinks[fullName]; ok {
   276  		return nil, fmt.Errorf("sink with name %q already exists", fullName)
   277  	}
   278  	h.setSink(fullName, req.Sink, req.UniqueWriterIdentity)
   279  	return req.Sink, nil
   280  }
   281  
   282  func (h *configHandler) setSink(name string, s *logpb.LogSink, uniqueWriterIdentity bool) {
   283  	if uniqueWriterIdentity {
   284  		s.WriterIdentity = "serviceAccount:" + name + "@gmail.com"
   285  	} else {
   286  		s.WriterIdentity = SharedServiceAccount
   287  	}
   288  	h.sinks[name] = s
   289  }
   290  
   291  // Creates or updates a sink.
   292  func (h *configHandler) UpdateSink(_ context.Context, req *logpb.UpdateSinkRequest) (*logpb.LogSink, error) {
   293  	h.mu.Lock()
   294  	defer h.mu.Unlock()
   295  	sink := h.sinks[req.SinkName]
   296  	// Update of a non-existent sink will create it.
   297  	if sink == nil {
   298  		h.setSink(req.SinkName, req.Sink, req.UniqueWriterIdentity)
   299  		sink = req.Sink
   300  	} else {
   301  		// sink is the existing sink named req.SinkName.
   302  		// Update all and only the fields of sink that are specified in the update mask.
   303  		paths := req.UpdateMask.GetPaths()
   304  		if len(paths) == 0 {
   305  			// An empty update mask is considered to have these fields by default.
   306  			paths = []string{"destination", "filter", "include_children"}
   307  		}
   308  		for _, p := range paths {
   309  			switch p {
   310  			case "destination":
   311  				sink.Destination = req.Sink.Destination
   312  			case "filter":
   313  				sink.Filter = req.Sink.Filter
   314  			case "include_children":
   315  				sink.IncludeChildren = req.Sink.IncludeChildren
   316  			case "output_version_format":
   317  				// noop
   318  			default:
   319  				return nil, fmt.Errorf("unknown path in mask: %q", p)
   320  			}
   321  		}
   322  		if req.UniqueWriterIdentity {
   323  			if sink.WriterIdentity != SharedServiceAccount {
   324  				return nil, invalidArgument("cannot change unique writer identity")
   325  			}
   326  			sink.WriterIdentity = "serviceAccount:" + req.SinkName + "@gmail.com"
   327  		}
   328  	}
   329  	return sink, nil
   330  
   331  }
   332  
   333  // Deletes a sink.
   334  func (h *configHandler) DeleteSink(_ context.Context, req *logpb.DeleteSinkRequest) (*emptypb.Empty, error) {
   335  	h.mu.Lock()
   336  	defer h.mu.Unlock()
   337  	delete(h.sinks, req.SinkName)
   338  	return &emptypb.Empty{}, nil
   339  }
   340  
   341  // Lists sinks. This fake implementation ignores the Parent field of
   342  // ListSinksRequest. All sinks are listed, regardless of their project.
   343  func (h *configHandler) ListSinks(_ context.Context, req *logpb.ListSinksRequest) (*logpb.ListSinksResponse, error) {
   344  	h.mu.Lock()
   345  	var sinks []*logpb.LogSink
   346  	for _, s := range h.sinks {
   347  		sinks = append(sinks, s)
   348  	}
   349  	h.mu.Unlock() // safe because no *logpb.LogSink is ever modified
   350  	// Since map iteration varies, sort the sinks.
   351  	sort.Sort(sinksByName(sinks))
   352  	from, to, nextPageToken, err := testutil.PageBounds(int(req.PageSize), req.PageToken, len(sinks))
   353  	if err != nil {
   354  		return nil, err
   355  	}
   356  	return &logpb.ListSinksResponse{
   357  		Sinks:         sinks[from:to],
   358  		NextPageToken: nextPageToken,
   359  	}, nil
   360  }
   361  
   362  type sinksByName []*logpb.LogSink
   363  
   364  func (s sinksByName) Len() int           { return len(s) }
   365  func (s sinksByName) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
   366  func (s sinksByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
   367  
   368  // Gets a metric.
   369  func (h *metricHandler) GetLogMetric(_ context.Context, req *logpb.GetLogMetricRequest) (*logpb.LogMetric, error) {
   370  	h.mu.Lock()
   371  	defer h.mu.Unlock()
   372  	if s, ok := h.metrics[req.MetricName]; ok {
   373  		return s, nil
   374  	}
   375  	// TODO(jba): use error codes
   376  	return nil, fmt.Errorf("metric %q not found", req.MetricName)
   377  }
   378  
   379  // Creates a metric.
   380  func (h *metricHandler) CreateLogMetric(_ context.Context, req *logpb.CreateLogMetricRequest) (*logpb.LogMetric, error) {
   381  	h.mu.Lock()
   382  	defer h.mu.Unlock()
   383  	fullName := fmt.Sprintf("%s/metrics/%s", req.Parent, req.Metric.Name)
   384  	if _, ok := h.metrics[fullName]; ok {
   385  		return nil, fmt.Errorf("metric with name %q already exists", fullName)
   386  	}
   387  	h.metrics[fullName] = req.Metric
   388  	return req.Metric, nil
   389  }
   390  
   391  // Creates or updates a metric.
   392  func (h *metricHandler) UpdateLogMetric(_ context.Context, req *logpb.UpdateLogMetricRequest) (*logpb.LogMetric, error) {
   393  	h.mu.Lock()
   394  	defer h.mu.Unlock()
   395  	// Update of a non-existent metric will create it.
   396  	h.metrics[req.MetricName] = req.Metric
   397  	return req.Metric, nil
   398  }
   399  
   400  // Deletes a metric.
   401  func (h *metricHandler) DeleteLogMetric(_ context.Context, req *logpb.DeleteLogMetricRequest) (*emptypb.Empty, error) {
   402  	h.mu.Lock()
   403  	defer h.mu.Unlock()
   404  	delete(h.metrics, req.MetricName)
   405  	return &emptypb.Empty{}, nil
   406  }
   407  
   408  // Lists metrics. This fake implementation ignores the Parent field of
   409  // ListMetricsRequest. All metrics are listed, regardless of their project.
   410  func (h *metricHandler) ListLogMetrics(_ context.Context, req *logpb.ListLogMetricsRequest) (*logpb.ListLogMetricsResponse, error) {
   411  	h.mu.Lock()
   412  	var metrics []*logpb.LogMetric
   413  	for _, s := range h.metrics {
   414  		metrics = append(metrics, s)
   415  	}
   416  	h.mu.Unlock() // safe because no *logpb.LogMetric is ever modified
   417  	// Since map iteration varies, sort the metrics.
   418  	sort.Sort(metricsByName(metrics))
   419  	from, to, nextPageToken, err := testutil.PageBounds(int(req.PageSize), req.PageToken, len(metrics))
   420  	if err != nil {
   421  		return nil, err
   422  	}
   423  	return &logpb.ListLogMetricsResponse{
   424  		Metrics:       metrics[from:to],
   425  		NextPageToken: nextPageToken,
   426  	}, nil
   427  }
   428  
   429  type metricsByName []*logpb.LogMetric
   430  
   431  func (s metricsByName) Len() int           { return len(s) }
   432  func (s metricsByName) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
   433  func (s metricsByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
   434  
   435  func invalidArgument(msg string) error {
   436  	// TODO(jba): status codes
   437  	return errors.New(msg)
   438  }
   439  

View as plain text