1/*
2Copyright 2016 Google LLC
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17// Package testing provides support for testing the logging client.
18package testing
19
20import (
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	emptypb "github.com/golang/protobuf/ptypes/empty"
32	tspb "github.com/golang/protobuf/ptypes/timestamp"
33	lpb "google.golang.org/genproto/googleapis/api/label"
34	mrpb "google.golang.org/genproto/googleapis/api/monitoredres"
35	logpb "google.golang.org/genproto/googleapis/logging/v2"
36)
37
38type loggingHandler struct {
39	logpb.LoggingServiceV2Server
40
41	mu   sync.Mutex
42	logs map[string][]*logpb.LogEntry // indexed by log name
43}
44
45type configHandler struct {
46	logpb.ConfigServiceV2Server
47
48	mu    sync.Mutex
49	sinks map[string]*logpb.LogSink // indexed by (full) sink name
50}
51
52type 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.
61func 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.
81func (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.
91const (
92	ValidProjectID = "PROJECT_ID"
93	ValidOrgID     = "433637338589"
94
95	SharedServiceAccount = "serviceAccount:cloud-logs@system.gserviceaccount.com"
96)
97
98// WriteLogEntries writes log entries to Stackdriver Logging. All log entries in
99// Stackdriver Logging are written by this method.
100func (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 Stackdriver Logging.
134//
135// This fake implementation ignores project IDs. It does not support full filtering, only
136// expressions of the form "logName = NAME".
137func (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
158func (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
173var filterRegexp = regexp.MustCompile(`^logName\s*=\s*"?([-_/.%\w]+)"?`)
174
175// returns the log name, or "" for the empty filter
176func 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
187func 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
202type byTimestamp []*logpb.LogEntry
203
204func (s byTimestamp) Len() int      { return len(s) }
205func (s byTimestamp) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
206func (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
218func 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 Stackdriver Logging.
226func (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.
242func (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.
260func (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.
271func (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
282func (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.
292func (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.
334func (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.
343func (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
362type sinksByName []*logpb.LogSink
363
364func (s sinksByName) Len() int           { return len(s) }
365func (s sinksByName) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
366func (s sinksByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
367
368// Gets a metric.
369func (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.
380func (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.
392func (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.
401func (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.
410func (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
429type metricsByName []*logpb.LogMetric
430
431func (s metricsByName) Len() int           { return len(s) }
432func (s metricsByName) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
433func (s metricsByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
434
435func invalidArgument(msg string) error {
436	// TODO(jba): status codes
437	return errors.New(msg)
438}
439