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