Skip to content

Commit 2e7f58b

Browse files
feat: log detail widget and log timestamp table cell renderer components
1 parent 202b00c commit 2e7f58b

12 files changed

+265
-4
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { createModelFactory, SpectatorModel } from '@hypertrace/dashboards/testing';
2+
import { runFakeRxjs } from '@hypertrace/test-utils';
3+
import { LogDetailDataSourceModel } from './log-detail-data-source.model';
4+
5+
describe('Log Detail data source model', () => {
6+
let spectator!: SpectatorModel<LogDetailDataSourceModel>;
7+
const buildModel = createModelFactory();
8+
spectator = buildModel(LogDetailDataSourceModel);
9+
spectator.model.logEvent = {
10+
attributes: {
11+
key1: 'value1',
12+
key2: 'value2'
13+
}
14+
};
15+
16+
test('test attribute data', () => {
17+
runFakeRxjs(({ expectObservable }) => {
18+
expectObservable(spectator.model.getData()).toBe('(x|)', {
19+
x: expect.objectContaining({
20+
key1: 'value1',
21+
key2: 'value2'
22+
})
23+
});
24+
});
25+
});
26+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Dictionary } from '@hypertrace/common';
2+
import { Model, ModelProperty, PLAIN_OBJECT_PROPERTY } from '@hypertrace/hyperdash';
3+
import { Observable, of } from 'rxjs';
4+
import { GraphQlDataSourceModel } from '../../../data/graphql/graphql-data-source.model';
5+
import { LogEvent } from '../../waterfall/waterfall/waterfall-chart';
6+
7+
@Model({
8+
type: 'log-detail-data-source'
9+
})
10+
export class LogDetailDataSourceModel extends GraphQlDataSourceModel<Dictionary<unknown>> {
11+
@ModelProperty({
12+
key: 'log-event',
13+
required: true,
14+
type: PLAIN_OBJECT_PROPERTY.type
15+
})
16+
public logEvent?: LogEvent;
17+
18+
public getData(): Observable<Dictionary<unknown>> {
19+
return of(this.logEvent?.attributes ?? {});
20+
}
21+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@import 'font';
2+
3+
.content {
4+
@include body-2-regular();
5+
margin-left: 175px;
6+
margin-top: 15px;
7+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { ChangeDetectionStrategy, Component } from '@angular/core';
2+
import { Dictionary } from '@hypertrace/common';
3+
import { ListViewHeader, ListViewRecord } from '@hypertrace/components';
4+
import { WidgetRenderer } from '@hypertrace/dashboards';
5+
import { Renderer } from '@hypertrace/hyperdash';
6+
import { Observable } from 'rxjs';
7+
import { LogDetailWidgetModel } from './log-detail-widget.model';
8+
9+
@Renderer({ modelClass: LogDetailWidgetModel })
10+
@Component({
11+
selector: 'ht-log-detail-widget-renderer',
12+
styleUrls: ['./log-detail-widget-renderer.component.scss'],
13+
changeDetection: ChangeDetectionStrategy.OnPush,
14+
template: `
15+
<div class="content" *htLoadAsync="this.data$ as attributes">
16+
<ht-list-view
17+
[records]="this.getLogEventAttributeRecords(attributes)"
18+
[header]="this.header"
19+
data-sensitive-pii
20+
></ht-list-view>
21+
</div>
22+
`
23+
})
24+
export class LogDetailWidgetRendererComponent extends WidgetRenderer<LogDetailWidgetModel, Dictionary<unknown>> {
25+
public readonly header: ListViewHeader = { keyLabel: 'key', valueLabel: 'value' };
26+
27+
protected fetchData(): Observable<Dictionary<unknown>> {
28+
return this.model.getData();
29+
}
30+
31+
public getLogEventAttributeRecords(attributes: Dictionary<unknown>): ListViewRecord[] {
32+
if (Boolean(attributes)) {
33+
return Object.entries(attributes).map((attribute: [string, unknown]) => ({
34+
key: attribute[0],
35+
value: attribute[1] as string | number
36+
}));
37+
}
38+
39+
return [];
40+
}
41+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Dictionary } from '@hypertrace/common';
2+
import { Model, ModelApi } from '@hypertrace/hyperdash';
3+
import { ModelInject, MODEL_API } from '@hypertrace/hyperdash-angular';
4+
import { Observable } from 'rxjs';
5+
6+
@Model({
7+
type: 'log-detail-widget',
8+
supportedDataSourceTypes: []
9+
})
10+
export class LogDetailWidgetModel {
11+
@ModelInject(MODEL_API)
12+
private readonly api!: ModelApi;
13+
14+
public getData(): Observable<Dictionary<unknown>> {
15+
return this.api.getData<Dictionary<unknown>>();
16+
}
17+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { CommonModule } from '@angular/common';
2+
import { NgModule } from '@angular/core';
3+
import { ListViewModule, LoadAsyncModule, SummaryValueModule, TitledContentModule } from '@hypertrace/components';
4+
import { DashboardCoreModule } from '@hypertrace/hyperdash-angular';
5+
import { SpanDetailModule } from '../../../components/span-detail/span-detail.module';
6+
import { LogDetailDataSourceModel } from './data/log-detail-data-source.model';
7+
import { LogDetailWidgetRendererComponent } from './log-detail-widget-renderer.component';
8+
import { LogDetailWidgetModel } from './log-detail-widget.model';
9+
10+
@NgModule({
11+
declarations: [LogDetailWidgetRendererComponent],
12+
imports: [
13+
DashboardCoreModule.with({
14+
models: [LogDetailWidgetModel, LogDetailDataSourceModel],
15+
renderers: [LogDetailWidgetRendererComponent]
16+
}),
17+
CommonModule,
18+
SpanDetailModule,
19+
TitledContentModule,
20+
ListViewModule,
21+
LoadAsyncModule,
22+
SummaryValueModule
23+
]
24+
})
25+
export class LogDetailWidgetModule {}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { NgModule } from '@angular/core';
2+
import { LogDetailWidgetModule } from './log-detail/log-detail-widget.module';
23
import { SpanDetailWidgetModule } from './span-detail/span-detail-widget.module';
34
import { TableWidgetModule } from './table/table-widget.module';
45
import { TraceDetailModule } from './trace-detail/trace-detail-widget.module';
56
import { WaterfallWidgetModule } from './waterfall/waterfall-widget.module';
67

78
@NgModule({
8-
imports: [SpanDetailWidgetModule, TableWidgetModule, TraceDetailModule, WaterfallWidgetModule]
9+
imports: [SpanDetailWidgetModule, LogDetailWidgetModule, TableWidgetModule, TraceDetailModule, WaterfallWidgetModule]
910
})
1011
export class TracingDashboardWidgetsModule {}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@import 'font';
2+
3+
.log-timestamp {
4+
@include ellipsis-overflow();
5+
6+
&.first-column {
7+
@include body-1-medium($gray-9);
8+
}
9+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {
2+
TableCellNoOpParser,
3+
tableCellProviders,
4+
tableCellRowDataProvider,
5+
TooltipDirective
6+
} from '@hypertrace/components';
7+
import { LogEvent } from '@hypertrace/distributed-tracing';
8+
import { createComponentFactory } from '@ngneat/spectator/jest';
9+
import { MockComponent } from 'ng-mocks';
10+
import { LogTimestampTableCellRendererComponent } from './log-timestamp-table-cell-renderer.component';
11+
12+
describe('log timestamp table cell renderer component', () => {
13+
const buildComponent = createComponentFactory({
14+
component: LogTimestampTableCellRendererComponent,
15+
providers: [
16+
tableCellProviders(
17+
{
18+
id: 'test'
19+
},
20+
new TableCellNoOpParser(undefined!)
21+
)
22+
],
23+
declarations: [MockComponent(TooltipDirective)],
24+
shallow: true
25+
});
26+
27+
test('testing component properties', () => {
28+
const logEvent: LogEvent = {
29+
timestamp: '2021-04-30T12:23:57.889149Z',
30+
spanStartTime: 1619785437887
31+
};
32+
const spectator = buildComponent({
33+
providers: [tableCellRowDataProvider(logEvent)]
34+
});
35+
36+
expect(spectator.queryAll('.log-timestamp')[0]).toContainText('2.89 ms');
37+
expect(spectator.component.spanStartTime).toBe(logEvent.spanStartTime);
38+
expect(spectator.component.timestamp).toBe(logEvent.timestamp);
39+
expect(spectator.component.duration).toBe('2.89 ms');
40+
expect(spectator.component.readableDateTime).toBe('2021/04/30 12:23:57.889');
41+
});
42+
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core';
2+
import { DateCoercer } from '@hypertrace/common';
3+
import {
4+
CoreTableCellParserType,
5+
TableCellAlignmentType,
6+
TableCellParserBase,
7+
TableCellRenderer,
8+
TableCellRendererBase,
9+
TableColumnConfig,
10+
TABLE_CELL_DATA,
11+
TABLE_COLUMN_CONFIG,
12+
TABLE_COLUMN_INDEX,
13+
TABLE_DATA_PARSER,
14+
TABLE_ROW_DATA
15+
} from '@hypertrace/components';
16+
import { LogEvent } from '@hypertrace/distributed-tracing';
17+
import { ObservabilityTableCellType } from '../../observability-table-cell-type';
18+
19+
@Component({
20+
selector: 'ht-log-timestamp-table-cell-renderer',
21+
styleUrls: ['./log-timestamp-table-cell-renderer.component.scss'],
22+
changeDetection: ChangeDetectionStrategy.OnPush,
23+
template: `
24+
<div class="log-timestamp" [htTooltip]="tooltip" [ngClass]="{ 'first-column': this.isFirstColumn }">
25+
{{ this.duration }}
26+
</div>
27+
<ng-template #tooltip>{{ this.readableDateTime }}</ng-template>
28+
`
29+
})
30+
@TableCellRenderer({
31+
type: ObservabilityTableCellType.LogTimestamp,
32+
alignment: TableCellAlignmentType.Left,
33+
parser: CoreTableCellParserType.NoOp
34+
})
35+
export class LogTimestampTableCellRendererComponent extends TableCellRendererBase<string> implements OnInit {
36+
public readonly spanStartTime: number;
37+
public readonly timestamp?: string;
38+
public readonly duration?: string;
39+
public readonly readableDateTime?: string;
40+
41+
private readonly dateCoercer = new DateCoercer();
42+
43+
public constructor(
44+
@Inject(TABLE_COLUMN_CONFIG) columnConfig: TableColumnConfig,
45+
@Inject(TABLE_COLUMN_INDEX) index: number,
46+
@Inject(TABLE_DATA_PARSER)
47+
parser: TableCellParserBase<string, string, unknown>,
48+
@Inject(TABLE_CELL_DATA) cellData: any,
49+
@Inject(TABLE_ROW_DATA) rowData: LogEvent
50+
) {
51+
super(columnConfig, index, parser, cellData, rowData);
52+
this.spanStartTime = rowData.spanStartTime as number;
53+
this.timestamp = rowData.timestamp;
54+
this.readableDateTime =
55+
this.timestamp
56+
?.replace('T', ' ')
57+
.replace('Z', ' ')
58+
.replace(/-/g, '/')
59+
.substr(0, this.timestamp.length - 8) + this.getDecimalMilliSeconds(this.timestamp, 3);
60+
61+
const date: Date = this.dateCoercer.coerce(this.timestamp) ?? new Date();
62+
const decimalMilliseconds: string = this.getDecimalMilliSeconds(this.timestamp, 2);
63+
this.duration = date.getTime() - this.spanStartTime + decimalMilliseconds + ' ms';
64+
}
65+
66+
private getDecimalMilliSeconds(timestamp: string = '', precision: number): string {
67+
return String(Number('0.' + timestamp.substr(timestamp.length - 7, 6)).toFixed(precision)).substring(1);
68+
}
69+
}

0 commit comments

Comments
 (0)