|
3 | 3 | using System.Data; |
4 | 4 | using Microsoft.Extensions.DependencyInjection; |
5 | 5 | using Microsoft.Extensions.Logging; |
| 6 | +using ServiceStack.Admin; |
6 | 7 | using ServiceStack.Data; |
7 | 8 | using ServiceStack.DataAnnotations; |
8 | 9 | using ServiceStack.Host; |
9 | 10 | using ServiceStack.OrmLite; |
| 11 | +using ServiceStack.Text; |
10 | 12 | using ServiceStack.Web; |
11 | 13 |
|
12 | 14 | namespace ServiceStack.Jobs; |
@@ -75,7 +77,7 @@ public object Any(AdminQueryRequestLogs request) |
75 | 77 | } |
76 | 78 |
|
77 | 79 | public class SqliteRequestLogger : InMemoryRollingRequestLogger, IRequiresSchema, |
78 | | - IRequireRegistration, IConfigureServices |
| 80 | + IRequireRegistration, IConfigureServices, IRequireAnalytics |
79 | 81 | { |
80 | 82 | private static readonly object dbWrites = Locks.RequestsDb; |
81 | 83 | public string DbDir { get; set; } = "App_Data/requests"; |
@@ -309,4 +311,198 @@ public static RequestLogEntry ToRequestLogEntry(RequestLog from) |
309 | 311 | Meta = from.Meta, |
310 | 312 | }; |
311 | 313 | } |
| 314 | + public static int AnalyticsBatchSize { get; set; } = 1000; |
| 315 | + |
| 316 | + public AnalyticsReports GetAnalyticsReports(DateTime month) |
| 317 | + { |
| 318 | + // op,user,tag,status,day,apikey,time(ms 0-50,51-100,101-200ms,1-2s,2s-5s,5s+) |
| 319 | + var ret = new AnalyticsReports(); |
| 320 | + using var db = OpenMonthDb(month); |
| 321 | + List<RequestLog> batch = []; |
| 322 | + long lastPk = 0; |
| 323 | + var metadata = HostContext.Metadata; |
| 324 | + |
| 325 | + void Add(Dictionary<string, RequestSummary> results, string name, RequestLog log) |
| 326 | + { |
| 327 | + var summary = results.TryGetValue(name, out var existing) |
| 328 | + ? existing |
| 329 | + : results[name] = new(); |
| 330 | + |
| 331 | + summary.Requests += 1; |
| 332 | + summary.Duration += log.RequestDuration.TotalMilliseconds; |
| 333 | + summary.RequestLength += log.RequestBody?.Length ?? 0; |
| 334 | + } |
| 335 | + void AddSummary(Dictionary<string, RequestSummary> results, string name, RequestSummary apiSummary) |
| 336 | + { |
| 337 | + var summary = results.TryGetValue(name, out var existing) |
| 338 | + ? existing |
| 339 | + : results[name] = new(); |
| 340 | + summary.Requests += apiSummary.Requests; |
| 341 | + summary.Duration += apiSummary.Duration; |
| 342 | + summary.RequestLength += apiSummary.RequestLength; |
| 343 | + } |
| 344 | + |
| 345 | + do { |
| 346 | + batch = db.Select( |
| 347 | + db.From<RequestLog>() |
| 348 | + .Where(x => x.Id > lastPk) |
| 349 | + .OrderBy(x => x.Id) |
| 350 | + .Limit(AnalyticsBatchSize)); |
| 351 | + foreach (var requestLog in batch) |
| 352 | + { |
| 353 | + Add(ret.Apis, requestLog.Request ?? requestLog.OperationName, requestLog); |
| 354 | + if (requestLog.StatusCode > 0) |
| 355 | + { |
| 356 | + var apiLog = ret.Apis[requestLog.Request ?? requestLog.OperationName]; |
| 357 | + apiLog.Status ??= new(); |
| 358 | + apiLog.Status[requestLog.StatusCode] = apiLog.Status.TryGetValue(requestLog.StatusCode, out var existing) |
| 359 | + ? existing + 1 |
| 360 | + : 1; |
| 361 | + } |
| 362 | + |
| 363 | + if (requestLog.UserAuthId != null) |
| 364 | + { |
| 365 | + Add(ret.Users, requestLog.UserAuthId, requestLog); |
| 366 | + if (requestLog.Meta?.TryGetValue("username", out var username) == true) |
| 367 | + { |
| 368 | + ret.Users[requestLog.UserAuthId].Name = username; |
| 369 | + } |
| 370 | + } |
| 371 | + |
| 372 | + if (requestLog.StatusCode > 0) |
| 373 | + { |
| 374 | + Add(ret.Status, requestLog.StatusCode.ToString(), requestLog); |
| 375 | + } |
| 376 | + |
| 377 | + Add(ret.Days, requestLog.DateTime.Day.ToString(), requestLog); |
| 378 | + |
| 379 | + if ((requestLog.Headers.TryGetValue(HttpHeaders.Authorization, out var authorization) && authorization.StartsWith("ak-")) || |
| 380 | + requestLog.Meta?.TryGetValue("apikey", out authorization) == true) |
| 381 | + { |
| 382 | + Add(ret.ApiKeys, authorization, requestLog); |
| 383 | + } |
| 384 | + |
| 385 | + if (requestLog.IpAddress != null) |
| 386 | + Add(ret.IpAddresses, requestLog.IpAddress, requestLog); |
| 387 | + |
| 388 | + //(ms 0-50,51-100,101-200ms,1-2s,2s-5s,5s+) |
| 389 | + int[] msRanges = [50, 100, 200, 1000, 2000, 5000, 30000]; |
| 390 | + var totalMs = (int)requestLog.RequestDuration.TotalMilliseconds; |
| 391 | + |
| 392 | + var added = false; |
| 393 | + foreach (var range in msRanges) |
| 394 | + { |
| 395 | + if (totalMs < range) |
| 396 | + { |
| 397 | + ret.DurationRange[range.ToString()] = ret.DurationRange.TryGetValue(range.ToString(), out var duration) |
| 398 | + ? duration + 1 |
| 399 | + : 1; |
| 400 | + added = true; |
| 401 | + break; |
| 402 | + } |
| 403 | + } |
| 404 | + if (!added) |
| 405 | + { |
| 406 | + var lastRange = ">" + msRanges.Last(); |
| 407 | + ret.DurationRange[lastRange] = ret.DurationRange.TryGetValue(lastRange, out var duration) |
| 408 | + ? duration + 1 |
| 409 | + : 1; |
| 410 | + } |
| 411 | + lastPk = requestLog.Id; |
| 412 | + } |
| 413 | + } while(batch.Count >= AnalyticsBatchSize); |
| 414 | + |
| 415 | + foreach (var entry in ret.Status) |
| 416 | + { |
| 417 | + if (int.TryParse(entry.Key, out var status)) |
| 418 | + { |
| 419 | + var desc = HttpStatus.GetStatusDescription(status); |
| 420 | + if (!string.IsNullOrEmpty(desc)) |
| 421 | + { |
| 422 | + entry.Value.Name = desc; |
| 423 | + } |
| 424 | + } |
| 425 | + } |
| 426 | + foreach (var requestDto in ret.Apis.Keys) |
| 427 | + { |
| 428 | + var requestType = metadata.GetRequestType(requestDto); |
| 429 | + if (requestType != null) |
| 430 | + { |
| 431 | + var op = metadata.GetOperation(requestType); |
| 432 | + if (op != null) |
| 433 | + { |
| 434 | + foreach (var tag in op.Tags) |
| 435 | + { |
| 436 | + AddSummary(ret.Tags, tag, ret.Apis[requestDto]); |
| 437 | + } |
| 438 | + } |
| 439 | + } |
| 440 | + } |
| 441 | + |
| 442 | + void Clean(Dictionary<string, RequestSummary> results) |
| 443 | + { |
| 444 | + foreach (var entry in results.Values) |
| 445 | + { |
| 446 | + entry.Duration = Math.Floor(entry.Duration); |
| 447 | + } |
| 448 | + } |
| 449 | + |
| 450 | + Clean(ret.Apis); |
| 451 | + Clean(ret.Users); |
| 452 | + Clean(ret.Tags); |
| 453 | + Clean(ret.Status); |
| 454 | + Clean(ret.Days); |
| 455 | + Clean(ret.ApiKeys); |
| 456 | + Clean(ret.IpAddresses); |
| 457 | + |
| 458 | + return ret; |
| 459 | + } |
| 460 | + |
| 461 | + public Dictionary<string, long> GetApiAnalytics(DateTime month, AnalyticsType type, string value) |
| 462 | + { |
| 463 | + using var db = OpenMonthDb(month); |
| 464 | + List<RequestLog> batch = []; |
| 465 | + long lastPk = 0; |
| 466 | + |
| 467 | + var ret = new Dictionary<string, long>(); |
| 468 | + |
| 469 | + do |
| 470 | + { |
| 471 | + var q = db.From<RequestLog>() |
| 472 | + .Where(x => x.Id > lastPk); |
| 473 | + |
| 474 | + if (type == AnalyticsType.User) |
| 475 | + { |
| 476 | + q.And(x => x.UserAuthId == value); |
| 477 | + } |
| 478 | + else if (type == AnalyticsType.Day) |
| 479 | + { |
| 480 | + var day = value.ToInt(); |
| 481 | + var from = month.WithDay(day); |
| 482 | + var to = from.AddDays(1); |
| 483 | + q.And(x => x.DateTime >= from && x.DateTime < to); |
| 484 | + } |
| 485 | + else if (type == AnalyticsType.ApiKey) |
| 486 | + { |
| 487 | + q.And("Headers LIKE {0}", $"%Bearer {value}%"); |
| 488 | + } |
| 489 | + else if (type == AnalyticsType.IpAddress) |
| 490 | + { |
| 491 | + q.And(x => x.IpAddress == value); |
| 492 | + } |
| 493 | + |
| 494 | + batch = db.Select(q |
| 495 | + .OrderBy(x => x.Id) |
| 496 | + .Limit(AnalyticsBatchSize)); |
| 497 | + foreach (var requestLog in batch) |
| 498 | + { |
| 499 | + var op = requestLog.Request ?? requestLog.OperationName; |
| 500 | + ret[op] = ret.TryGetValue(op, out var existing) |
| 501 | + ? existing + 1 |
| 502 | + : 1; |
| 503 | + lastPk = requestLog.Id; |
| 504 | + } |
| 505 | + } while(batch.Count >= AnalyticsBatchSize); |
| 506 | + return ret; |
| 507 | + } |
312 | 508 | } |
0 commit comments