@oneuptime/common 10.0.30 → 10.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/Models/AnalyticsModels/ExceptionInstance.ts +29 -4
  2. package/Models/AnalyticsModels/Log.ts +110 -4
  3. package/Models/AnalyticsModels/Metric.ts +16 -9
  4. package/Models/AnalyticsModels/MonitorLog.ts +4 -2
  5. package/Models/AnalyticsModels/Span.ts +79 -6
  6. package/Models/DatabaseModels/Index.ts +8 -0
  7. package/Models/DatabaseModels/LogDropFilter.ts +480 -0
  8. package/Models/DatabaseModels/LogPipeline.ts +412 -0
  9. package/Models/DatabaseModels/LogPipelineProcessor.ts +430 -0
  10. package/Models/DatabaseModels/LogScrubRule.ts +516 -0
  11. package/Server/API/TelemetryAPI.ts +261 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1773402621107-MigrationName.ts +131 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.ts +79 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.ts +41 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1773676206197-MigrationName.ts +57 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  17. package/Server/Services/AnalyticsDatabaseService.ts +61 -0
  18. package/Server/Services/LogAggregationService.ts +238 -1
  19. package/Server/Services/LogDropFilterService.ts +10 -0
  20. package/Server/Services/LogPipelineProcessorService.ts +10 -0
  21. package/Server/Services/LogPipelineService.ts +10 -0
  22. package/Server/Services/LogScrubRuleService.ts +10 -0
  23. package/Server/Services/TelemetryAttributeService.ts +4 -6
  24. package/Server/Utils/AnalyticsDatabase/Statement.ts +15 -1
  25. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +126 -11
  26. package/Tests/Server/Services/LogAggregationService.test.ts +3 -2
  27. package/Types/AnalyticsDatabase/AnalyticsTableName.ts +9 -0
  28. package/Types/AnalyticsDatabase/TableColumnType.ts +4 -0
  29. package/Types/Date.ts +22 -0
  30. package/Types/Log/LogDropFilterAction.ts +6 -0
  31. package/Types/Log/LogPipelineProcessorType.ts +44 -0
  32. package/Types/Log/LogScrubAction.ts +7 -0
  33. package/Types/Log/LogScrubPatternType.ts +10 -0
  34. package/Types/Permission.ts +174 -0
  35. package/UI/Components/LogsViewer/LogsViewer.tsx +152 -4
  36. package/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.tsx +92 -0
  37. package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +332 -117
  38. package/UI/Components/LogsViewer/components/LogSearchBar.tsx +294 -274
  39. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +513 -234
  40. package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +37 -29
  41. package/UI/Components/LogsViewer/components/LogsTable.tsx +6 -1
  42. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +106 -0
  43. package/UI/Utils/LogExport.ts +160 -0
  44. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +28 -4
  45. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
  46. package/build/dist/Models/AnalyticsModels/Log.js +97 -4
  47. package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
  48. package/build/dist/Models/AnalyticsModels/Metric.js +16 -9
  49. package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
  50. package/build/dist/Models/AnalyticsModels/MonitorLog.js +4 -2
  51. package/build/dist/Models/AnalyticsModels/MonitorLog.js.map +1 -1
  52. package/build/dist/Models/AnalyticsModels/Span.js +73 -6
  53. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  54. package/build/dist/Models/DatabaseModels/Index.js +8 -0
  55. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  56. package/build/dist/Models/DatabaseModels/LogDropFilter.js +508 -0
  57. package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -0
  58. package/build/dist/Models/DatabaseModels/LogPipeline.js +438 -0
  59. package/build/dist/Models/DatabaseModels/LogPipeline.js.map +1 -0
  60. package/build/dist/Models/DatabaseModels/LogPipelineProcessor.js +452 -0
  61. package/build/dist/Models/DatabaseModels/LogPipelineProcessor.js.map +1 -0
  62. package/build/dist/Models/DatabaseModels/LogScrubRule.js +545 -0
  63. package/build/dist/Models/DatabaseModels/LogScrubRule.js.map +1 -0
  64. package/build/dist/Server/API/TelemetryAPI.js +155 -0
  65. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  66. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773402621107-MigrationName.js +52 -0
  67. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773402621107-MigrationName.js.map +1 -0
  68. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.js +34 -0
  69. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.js.map +1 -0
  70. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.js +22 -0
  71. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.js.map +1 -0
  72. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773676206197-MigrationName.js +26 -0
  73. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773676206197-MigrationName.js.map +1 -0
  74. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  75. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  76. package/build/dist/Server/Services/AnalyticsDatabaseService.js +30 -0
  77. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  78. package/build/dist/Server/Services/LogAggregationService.js +188 -1
  79. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  80. package/build/dist/Server/Services/LogDropFilterService.js +9 -0
  81. package/build/dist/Server/Services/LogDropFilterService.js.map +1 -0
  82. package/build/dist/Server/Services/LogPipelineProcessorService.js +9 -0
  83. package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -0
  84. package/build/dist/Server/Services/LogPipelineService.js +9 -0
  85. package/build/dist/Server/Services/LogPipelineService.js.map +1 -0
  86. package/build/dist/Server/Services/LogScrubRuleService.js +9 -0
  87. package/build/dist/Server/Services/LogScrubRuleService.js.map +1 -0
  88. package/build/dist/Server/Services/TelemetryAttributeService.js +4 -6
  89. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  90. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +13 -1
  91. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  92. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +89 -2
  93. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  94. package/build/dist/Tests/Server/Services/LogAggregationService.test.js +3 -2
  95. package/build/dist/Tests/Server/Services/LogAggregationService.test.js.map +1 -1
  96. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +10 -0
  97. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -0
  98. package/build/dist/Types/AnalyticsDatabase/TableColumnType.js +4 -0
  99. package/build/dist/Types/AnalyticsDatabase/TableColumnType.js.map +1 -1
  100. package/build/dist/Types/Date.js +16 -0
  101. package/build/dist/Types/Date.js.map +1 -1
  102. package/build/dist/Types/Log/LogDropFilterAction.js +7 -0
  103. package/build/dist/Types/Log/LogDropFilterAction.js.map +1 -0
  104. package/build/dist/Types/Log/LogPipelineProcessorType.js +9 -0
  105. package/build/dist/Types/Log/LogPipelineProcessorType.js.map +1 -0
  106. package/build/dist/Types/Log/LogScrubAction.js +8 -0
  107. package/build/dist/Types/Log/LogScrubAction.js.map +1 -0
  108. package/build/dist/Types/Log/LogScrubPatternType.js +11 -0
  109. package/build/dist/Types/Log/LogScrubPatternType.js.map +1 -0
  110. package/build/dist/Types/Permission.js +152 -0
  111. package/build/dist/Types/Permission.js.map +1 -1
  112. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +124 -11
  113. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  114. package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js +36 -0
  115. package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js.map +1 -0
  116. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +114 -4
  117. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
  118. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +17 -5
  119. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -1
  120. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +229 -122
  121. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  122. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +5 -4
  123. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
  124. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +4 -1
  125. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
  126. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +28 -0
  127. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  128. package/build/dist/UI/Utils/LogExport.js +129 -0
  129. package/build/dist/UI/Utils/LogExport.js.map +1 -0
  130. package/package.json +1 -1
@@ -0,0 +1,44 @@
1
+ enum LogPipelineProcessorType {
2
+ GrokParser = "GrokParser",
3
+ AttributeRemapper = "AttributeRemapper",
4
+ SeverityRemapper = "SeverityRemapper",
5
+ CategoryProcessor = "CategoryProcessor",
6
+ }
7
+
8
+ export interface GrokParserConfig {
9
+ source: string; // field to parse, e.g. "body"
10
+ pattern: string; // grok pattern
11
+ targetPrefix?: string; // prefix for extracted attributes
12
+ }
13
+
14
+ export interface AttributeRemapperConfig {
15
+ sourceKey: string; // source attribute key
16
+ targetKey: string; // target attribute key
17
+ preserveSource?: boolean; // keep original key (default false)
18
+ overrideOnConflict?: boolean; // overwrite if target exists (default true)
19
+ }
20
+
21
+ export interface SeverityRemapperConfig {
22
+ sourceKey: string; // attribute key containing severity info
23
+ mappings: Array<{
24
+ matchValue: string; // value to match (case-insensitive)
25
+ severityText: string; // mapped severity text
26
+ severityNumber: number; // mapped severity number
27
+ }>;
28
+ }
29
+
30
+ export interface CategoryProcessorConfig {
31
+ targetKey: string; // attribute key to store the category
32
+ categories: Array<{
33
+ name: string; // category name/value
34
+ filterQuery: string; // condition to match
35
+ }>;
36
+ }
37
+
38
+ export type LogPipelineProcessorConfig =
39
+ | GrokParserConfig
40
+ | AttributeRemapperConfig
41
+ | SeverityRemapperConfig
42
+ | CategoryProcessorConfig;
43
+
44
+ export default LogPipelineProcessorType;
@@ -0,0 +1,7 @@
1
+ enum LogScrubAction {
2
+ Mask = "mask",
3
+ Hash = "hash",
4
+ Redact = "redact",
5
+ }
6
+
7
+ export default LogScrubAction;
@@ -0,0 +1,10 @@
1
+ enum LogScrubPatternType {
2
+ Email = "email",
3
+ CreditCard = "creditCard",
4
+ SSN = "ssn",
5
+ PhoneNumber = "phoneNumber",
6
+ IPAddress = "ipAddress",
7
+ Custom = "custom",
8
+ }
9
+
10
+ export default LogScrubPatternType;
@@ -82,6 +82,30 @@ enum Permission {
82
82
  EditTelemetryServiceLog = "EditTelemetryServiceLog",
83
83
  ReadTelemetryServiceLog = "ReadTelemetryServiceLog",
84
84
 
85
+ // Log Pipelines
86
+ CreateProjectLogPipeline = "CreateProjectLogPipeline",
87
+ DeleteProjectLogPipeline = "DeleteProjectLogPipeline",
88
+ EditProjectLogPipeline = "EditProjectLogPipeline",
89
+ ReadProjectLogPipeline = "ReadProjectLogPipeline",
90
+
91
+ // Log Pipeline Processors
92
+ CreateProjectLogPipelineProcessor = "CreateProjectLogPipelineProcessor",
93
+ DeleteProjectLogPipelineProcessor = "DeleteProjectLogPipelineProcessor",
94
+ EditProjectLogPipelineProcessor = "EditProjectLogPipelineProcessor",
95
+ ReadProjectLogPipelineProcessor = "ReadProjectLogPipelineProcessor",
96
+
97
+ // Log Drop Filters
98
+ CreateProjectLogDropFilter = "CreateProjectLogDropFilter",
99
+ DeleteProjectLogDropFilter = "DeleteProjectLogDropFilter",
100
+ EditProjectLogDropFilter = "EditProjectLogDropFilter",
101
+ ReadProjectLogDropFilter = "ReadProjectLogDropFilter",
102
+
103
+ // Log Scrub Rules
104
+ CreateProjectLogScrubRule = "CreateProjectLogScrubRule",
105
+ DeleteProjectLogScrubRule = "DeleteProjectLogScrubRule",
106
+ EditProjectLogScrubRule = "EditProjectLogScrubRule",
107
+ ReadProjectLogScrubRule = "ReadProjectLogScrubRule",
108
+
85
109
  // Exceptions
86
110
  CreateTelemetryException = "CreateTelemetryException",
87
111
  DeleteTelemetryException = "DeleteTelemetryException",
@@ -3981,6 +4005,156 @@ export class PermissionHelper {
3981
4005
  group: PermissionGroup.Telemetry,
3982
4006
  },
3983
4007
 
4008
+ // Log Pipeline Permissions
4009
+ {
4010
+ permission: Permission.CreateProjectLogPipeline,
4011
+ title: "Create Log Pipeline",
4012
+ description:
4013
+ "This permission can create Log Pipelines in this project.",
4014
+ isAssignableToTenant: true,
4015
+ isAccessControlPermission: false,
4016
+ group: PermissionGroup.Telemetry,
4017
+ },
4018
+ {
4019
+ permission: Permission.DeleteProjectLogPipeline,
4020
+ title: "Delete Log Pipeline",
4021
+ description:
4022
+ "This permission can delete Log Pipelines of this project.",
4023
+ isAssignableToTenant: true,
4024
+ isAccessControlPermission: false,
4025
+ group: PermissionGroup.Telemetry,
4026
+ },
4027
+ {
4028
+ permission: Permission.EditProjectLogPipeline,
4029
+ title: "Edit Log Pipeline",
4030
+ description: "This permission can edit Log Pipelines of this project.",
4031
+ isAssignableToTenant: true,
4032
+ isAccessControlPermission: false,
4033
+ group: PermissionGroup.Telemetry,
4034
+ },
4035
+ {
4036
+ permission: Permission.ReadProjectLogPipeline,
4037
+ title: "Read Log Pipeline",
4038
+ description: "This permission can read Log Pipelines of this project.",
4039
+ isAssignableToTenant: true,
4040
+ isAccessControlPermission: false,
4041
+ group: PermissionGroup.Telemetry,
4042
+ },
4043
+
4044
+ // Log Pipeline Processor Permissions
4045
+ {
4046
+ permission: Permission.CreateProjectLogPipelineProcessor,
4047
+ title: "Create Log Pipeline Processor",
4048
+ description:
4049
+ "This permission can create Log Pipeline Processors in this project.",
4050
+ isAssignableToTenant: true,
4051
+ isAccessControlPermission: false,
4052
+ group: PermissionGroup.Telemetry,
4053
+ },
4054
+ {
4055
+ permission: Permission.DeleteProjectLogPipelineProcessor,
4056
+ title: "Delete Log Pipeline Processor",
4057
+ description:
4058
+ "This permission can delete Log Pipeline Processors of this project.",
4059
+ isAssignableToTenant: true,
4060
+ isAccessControlPermission: false,
4061
+ group: PermissionGroup.Telemetry,
4062
+ },
4063
+ {
4064
+ permission: Permission.EditProjectLogPipelineProcessor,
4065
+ title: "Edit Log Pipeline Processor",
4066
+ description:
4067
+ "This permission can edit Log Pipeline Processors of this project.",
4068
+ isAssignableToTenant: true,
4069
+ isAccessControlPermission: false,
4070
+ group: PermissionGroup.Telemetry,
4071
+ },
4072
+ {
4073
+ permission: Permission.ReadProjectLogPipelineProcessor,
4074
+ title: "Read Log Pipeline Processor",
4075
+ description:
4076
+ "This permission can read Log Pipeline Processors of this project.",
4077
+ isAssignableToTenant: true,
4078
+ isAccessControlPermission: false,
4079
+ group: PermissionGroup.Telemetry,
4080
+ },
4081
+
4082
+ // Log Drop Filter Permissions
4083
+ {
4084
+ permission: Permission.CreateProjectLogDropFilter,
4085
+ title: "Create Log Drop Filter",
4086
+ description:
4087
+ "This permission can create Log Drop Filters in this project.",
4088
+ isAssignableToTenant: true,
4089
+ isAccessControlPermission: false,
4090
+ group: PermissionGroup.Telemetry,
4091
+ },
4092
+ {
4093
+ permission: Permission.DeleteProjectLogDropFilter,
4094
+ title: "Delete Log Drop Filter",
4095
+ description:
4096
+ "This permission can delete Log Drop Filters of this project.",
4097
+ isAssignableToTenant: true,
4098
+ isAccessControlPermission: false,
4099
+ group: PermissionGroup.Telemetry,
4100
+ },
4101
+ {
4102
+ permission: Permission.EditProjectLogDropFilter,
4103
+ title: "Edit Log Drop Filter",
4104
+ description:
4105
+ "This permission can edit Log Drop Filters of this project.",
4106
+ isAssignableToTenant: true,
4107
+ isAccessControlPermission: false,
4108
+ group: PermissionGroup.Telemetry,
4109
+ },
4110
+ {
4111
+ permission: Permission.ReadProjectLogDropFilter,
4112
+ title: "Read Log Drop Filter",
4113
+ description:
4114
+ "This permission can read Log Drop Filters of this project.",
4115
+ isAssignableToTenant: true,
4116
+ isAccessControlPermission: false,
4117
+ group: PermissionGroup.Telemetry,
4118
+ },
4119
+
4120
+ // Log Scrub Rule Permissions
4121
+ {
4122
+ permission: Permission.CreateProjectLogScrubRule,
4123
+ title: "Create Log Scrub Rule",
4124
+ description:
4125
+ "This permission can create Log Scrub Rules in this project.",
4126
+ isAssignableToTenant: true,
4127
+ isAccessControlPermission: false,
4128
+ group: PermissionGroup.Telemetry,
4129
+ },
4130
+ {
4131
+ permission: Permission.DeleteProjectLogScrubRule,
4132
+ title: "Delete Log Scrub Rule",
4133
+ description:
4134
+ "This permission can delete Log Scrub Rules of this project.",
4135
+ isAssignableToTenant: true,
4136
+ isAccessControlPermission: false,
4137
+ group: PermissionGroup.Telemetry,
4138
+ },
4139
+ {
4140
+ permission: Permission.EditProjectLogScrubRule,
4141
+ title: "Edit Log Scrub Rule",
4142
+ description:
4143
+ "This permission can edit Log Scrub Rules of this project.",
4144
+ isAssignableToTenant: true,
4145
+ isAccessControlPermission: false,
4146
+ group: PermissionGroup.Telemetry,
4147
+ },
4148
+ {
4149
+ permission: Permission.ReadProjectLogScrubRule,
4150
+ title: "Read Log Scrub Rule",
4151
+ description:
4152
+ "This permission can read Log Scrub Rules of this project.",
4153
+ isAssignableToTenant: true,
4154
+ isAccessControlPermission: false,
4155
+ group: PermissionGroup.Telemetry,
4156
+ },
4157
+
3984
4158
  {
3985
4159
  permission: Permission.CreateTelemetryException,
3986
4160
  title: "Create Telemetry Service Exception",
@@ -4,6 +4,7 @@ import React, {
4
4
  useCallback,
5
5
  useEffect,
6
6
  useMemo,
7
+ useRef,
7
8
  useState,
8
9
  } from "react";
9
10
  import Query from "../../../Types/BaseDatabase/Query";
@@ -51,9 +52,12 @@ import {
51
52
  normalizeLogsTableColumns,
52
53
  } from "./types";
53
54
  import LogsAnalyticsView from "./components/LogsAnalyticsView";
55
+ import { LogSearchBarRef } from "./components/LogSearchBar";
54
56
  import { queryStringToFilter } from "../../../Types/Log/LogQueryToFilter";
55
57
  import RangeStartAndEndDateTime from "../../../Types/Time/RangeStartAndEndDateTime";
56
58
  import TimeRange from "../../../Types/Time/TimeRange";
59
+ import { exportLogs, LogExportFormat } from "../../Utils/LogExport";
60
+ import ObjectID from "../../../Types/ObjectID";
57
61
 
58
62
  export interface ComponentProps {
59
63
  logs: Array<Log>;
@@ -64,6 +68,7 @@ export interface ComponentProps {
64
68
  noLogsMessage?: string | undefined;
65
69
  getTraceRoute?: (traceId: string, log: Log) => Route | URL | undefined;
66
70
  getSpanRoute?: (spanId: string, log: Log) => Route | URL | undefined;
71
+ projectId?: ObjectID | undefined;
67
72
  totalCount?: number | undefined;
68
73
  page?: number | undefined;
69
74
  pageSize?: number | undefined;
@@ -195,6 +200,9 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
195
200
  const [serviceMap, setServiceMap] = useState<Dictionary<Service>>({});
196
201
 
197
202
  const [selectedLogId, setSelectedLogId] = useState<string | null>(null);
203
+ const [focusedRowIndex, setFocusedRowIndex] = useState<number>(-1);
204
+ const searchBarRef: React.RefObject<LogSearchBarRef> =
205
+ useRef<LogSearchBarRef>(null!);
198
206
 
199
207
  const [internalPage, setInternalPage] = useState<number>(1);
200
208
  const [internalPageSize, setInternalPageSize] =
@@ -211,6 +219,9 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
211
219
  const [internalViewMode, setInternalViewMode] =
212
220
  useState<LogsViewMode>("list");
213
221
 
222
+ const [showKeyboardShortcuts, setShowKeyboardShortcuts] =
223
+ useState<boolean>(false);
224
+
214
225
  useEffect(() => {
215
226
  setFilterData(props.filterData);
216
227
  }, [props.filterData]);
@@ -425,7 +436,12 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
425
436
  }
426
437
  }, [attributesLoaded, attributesLoading, loadAttributes]);
427
438
 
428
- const resetPage: () => void = (): void => {
439
+ // Reset focused row when displayed logs change
440
+ useEffect(() => {
441
+ setFocusedRowIndex(-1);
442
+ }, [displayedLogs]);
443
+
444
+ const resetPage: () => void = useCallback((): void => {
429
445
  if (props.onPageChange) {
430
446
  props.onPageChange(1);
431
447
  }
@@ -433,9 +449,9 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
433
449
  if (props.page === undefined) {
434
450
  setInternalPage(1);
435
451
  }
436
- };
452
+ }, [props.onPageChange, props.page]);
437
453
 
438
- const handleSearchSubmit: () => void = (): void => {
454
+ const handleSearchSubmit: () => void = useCallback((): void => {
439
455
  const queryFilter: Record<string, unknown> = queryStringToFilter(
440
456
  searchQuery,
441
457
  ) as Record<string, unknown>;
@@ -464,7 +480,122 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
464
480
  resetPage();
465
481
  setSelectedLogId(null);
466
482
  props.onFilterChanged(mergedFilter);
467
- };
483
+ }, [searchQuery, serviceMap, filterData, resetPage, props]);
484
+
485
+ // Scroll focused row into view
486
+ useEffect(() => {
487
+ if (focusedRowIndex < 0) {
488
+ return;
489
+ }
490
+
491
+ // Use requestAnimationFrame to ensure the DOM has updated with data-focused
492
+ requestAnimationFrame(() => {
493
+ const focusedRow: Element | null = document.querySelector(
494
+ `tr[data-focused="true"]`,
495
+ );
496
+ if (focusedRow) {
497
+ focusedRow.scrollIntoView({ block: "nearest" });
498
+ }
499
+ });
500
+ }, [focusedRowIndex]);
501
+
502
+ // Keyboard shortcuts: j/k navigate, Enter expand/collapse, Escape close, / focus search, Ctrl+Enter apply
503
+ useEffect(() => {
504
+ const handleKeyDown: (e: KeyboardEvent) => void = (
505
+ e: KeyboardEvent,
506
+ ): void => {
507
+ const target: EventTarget | null = e.target;
508
+ const isInputFocused: boolean =
509
+ target instanceof HTMLInputElement ||
510
+ target instanceof HTMLTextAreaElement ||
511
+ target instanceof HTMLSelectElement ||
512
+ (target instanceof HTMLElement && target.isContentEditable);
513
+
514
+ // Ctrl+Enter applies filters even when search bar is focused
515
+ if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
516
+ e.preventDefault();
517
+ handleSearchSubmit();
518
+ return;
519
+ }
520
+
521
+ // Skip other shortcuts when an input is focused
522
+ if (isInputFocused) {
523
+ return;
524
+ }
525
+
526
+ if (e.key === "/") {
527
+ e.preventDefault();
528
+ searchBarRef.current?.focus();
529
+ return;
530
+ }
531
+
532
+ if (e.key === "?") {
533
+ e.preventDefault();
534
+ setShowKeyboardShortcuts((prev: boolean) => {
535
+ return !prev;
536
+ });
537
+ return;
538
+ }
539
+
540
+ if (e.key === "Escape") {
541
+ if (showKeyboardShortcuts) {
542
+ setShowKeyboardShortcuts(false);
543
+ return;
544
+ }
545
+ if (selectedLogId) {
546
+ setSelectedLogId(null);
547
+ }
548
+ return;
549
+ }
550
+
551
+ const logCount: number = displayedLogs.length;
552
+
553
+ if (logCount === 0) {
554
+ return;
555
+ }
556
+
557
+ if (e.key === "j") {
558
+ e.preventDefault();
559
+ setFocusedRowIndex((prev: number): number => {
560
+ return Math.min(prev + 1, logCount - 1);
561
+ });
562
+ return;
563
+ }
564
+
565
+ if (e.key === "k") {
566
+ e.preventDefault();
567
+ setFocusedRowIndex((prev: number): number => {
568
+ return Math.max(prev - 1, 0);
569
+ });
570
+ return;
571
+ }
572
+
573
+ if (e.key === "Enter") {
574
+ if (focusedRowIndex >= 0 && focusedRowIndex < logCount) {
575
+ const log: Log = displayedLogs[focusedRowIndex]!;
576
+ const rowId: string = resolveLogIdentifier(log, focusedRowIndex);
577
+ setSelectedLogId((currentSelected: string | null) => {
578
+ if (currentSelected === rowId) {
579
+ return null;
580
+ }
581
+ return rowId;
582
+ });
583
+ }
584
+ return;
585
+ }
586
+ };
587
+
588
+ document.addEventListener("keydown", handleKeyDown);
589
+ return () => {
590
+ document.removeEventListener("keydown", handleKeyDown);
591
+ };
592
+ }, [
593
+ displayedLogs,
594
+ focusedRowIndex,
595
+ selectedLogId,
596
+ showKeyboardShortcuts,
597
+ handleSearchSubmit,
598
+ ]);
468
599
 
469
600
  const handlePageChange: (page: number) => void = (page: number): void => {
470
601
  if (props.onPageChange) {
@@ -655,6 +786,12 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
655
786
  },
656
787
  }
657
788
  : {}),
789
+ onExportCSV: () => {
790
+ exportLogs(displayedLogs, LogExportFormat.CSV, selectedColumns);
791
+ },
792
+ onExportJSON: () => {
793
+ exportLogs(displayedLogs, LogExportFormat.JSON, selectedColumns);
794
+ },
658
795
  ...(props.liveOptions ? { liveOptions: props.liveOptions } : {}),
659
796
  ...(props.timeRange && props.onTimeRangeChange
660
797
  ? {
@@ -662,6 +799,12 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
662
799
  onTimeRangeChange: props.onTimeRangeChange,
663
800
  }
664
801
  : {}),
802
+ showKeyboardShortcuts,
803
+ onToggleKeyboardShortcuts: () => {
804
+ setShowKeyboardShortcuts((prev: boolean) => {
805
+ return !prev;
806
+ });
807
+ },
665
808
  };
666
809
 
667
810
  const showSidebar: boolean =
@@ -672,6 +815,7 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
672
815
  {props.showFilters && (
673
816
  <div>
674
817
  <LogsFilterCard
818
+ ref={searchBarRef}
675
819
  logAttributes={logAttributes}
676
820
  searchQuery={searchQuery}
677
821
  onSearchQueryChange={setSearchQuery}
@@ -737,6 +881,9 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
737
881
  logs={displayedLogs}
738
882
  serviceMap={serviceMap}
739
883
  isLoading={props.isLoading}
884
+ focusedRowIndex={
885
+ focusedRowIndex >= 0 ? focusedRowIndex : undefined
886
+ }
740
887
  emptyMessage={
741
888
  props.noLogsMessage ||
742
889
  getEmptyMessageWithTimeRange(props.timeRange)
@@ -766,6 +913,7 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
766
913
  getTraceRoute={props.getTraceRoute}
767
914
  getSpanRoute={props.getSpanRoute}
768
915
  variant="embedded"
916
+ projectId={props.projectId}
769
917
  />
770
918
  );
771
919
  }}
@@ -0,0 +1,92 @@
1
+ import React, { FunctionComponent, ReactElement } from "react";
2
+
3
+ export interface KeyboardShortcutsHelpProps {
4
+ onClose: () => void;
5
+ }
6
+
7
+ interface ShortcutRow {
8
+ keys: Array<string>;
9
+ description: string;
10
+ }
11
+
12
+ const SHORTCUT_ROWS: Array<ShortcutRow> = [
13
+ { keys: ["j"], description: "Move to next log row" },
14
+ { keys: ["k"], description: "Move to previous log row" },
15
+ { keys: ["Enter"], description: "Expand / collapse selected log" },
16
+ { keys: ["Esc"], description: "Close detail panel" },
17
+ { keys: ["/"], description: "Focus search bar" },
18
+ { keys: ["Ctrl", "Enter"], description: "Apply search filters" },
19
+ { keys: ["?"], description: "Toggle this help" },
20
+ ];
21
+
22
+ const KeyboardShortcutsHelp: FunctionComponent<KeyboardShortcutsHelpProps> = (
23
+ props: KeyboardShortcutsHelpProps,
24
+ ): ReactElement => {
25
+ return (
26
+ <div className="absolute right-0 top-full z-50 mt-1 w-72 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg">
27
+ <div className="flex items-center justify-between border-b border-gray-100 px-3 py-2">
28
+ <span className="text-[11px] font-semibold uppercase tracking-wider text-gray-400">
29
+ Keyboard shortcuts
30
+ </span>
31
+ <button
32
+ type="button"
33
+ className="text-gray-400 transition-colors hover:text-gray-600"
34
+ onClick={props.onClose}
35
+ >
36
+ <svg
37
+ className="h-3.5 w-3.5"
38
+ fill="none"
39
+ viewBox="0 0 24 24"
40
+ strokeWidth={2}
41
+ stroke="currentColor"
42
+ >
43
+ <path
44
+ strokeLinecap="round"
45
+ strokeLinejoin="round"
46
+ d="M6 18 18 6M6 6l12 12"
47
+ />
48
+ </svg>
49
+ </button>
50
+ </div>
51
+
52
+ <div className="py-1">
53
+ {SHORTCUT_ROWS.map((row: ShortcutRow) => {
54
+ return (
55
+ <div
56
+ key={row.description}
57
+ className="flex items-center justify-between px-3 py-1.5"
58
+ >
59
+ <span className="text-xs text-gray-600">{row.description}</span>
60
+ <div className="flex items-center gap-1">
61
+ {row.keys.map((key: string, index: number) => {
62
+ return (
63
+ <React.Fragment key={key}>
64
+ {index > 0 && (
65
+ <span className="text-[10px] text-gray-400">+</span>
66
+ )}
67
+ <kbd className="inline-flex min-w-[1.5rem] items-center justify-center rounded border border-gray-200 bg-gray-50 px-1.5 py-0.5 font-mono text-[11px] font-medium text-gray-600">
68
+ {key}
69
+ </kbd>
70
+ </React.Fragment>
71
+ );
72
+ })}
73
+ </div>
74
+ </div>
75
+ );
76
+ })}
77
+ </div>
78
+
79
+ <div className="border-t border-gray-100 px-3 py-1.5">
80
+ <span className="text-[10px] text-gray-400">
81
+ Press{" "}
82
+ <kbd className="rounded border border-gray-200 bg-gray-50 px-1 py-0.5 font-mono text-[10px]">
83
+ ?
84
+ </kbd>{" "}
85
+ to toggle this panel
86
+ </span>
87
+ </div>
88
+ </div>
89
+ );
90
+ };
91
+
92
+ export default KeyboardShortcutsHelp;