Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add metrics support (with only minimum basic features) #98

Merged
merged 4 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# otel-tui

🚧 This project is under construction 🚧

A terminal OpenTelemetry viewer inspired by [otel-desktop-viewer](https://github.com/CtrlSpice/otel-desktop-viewer/tree/main)

Traces
![Traces](./docs/traces.png)
![Spans](./docs/spans.png)

Metrics
![Metrics](./docs/metrics.png)

Logs
![Logs](./docs/logs.png)

Expand Down Expand Up @@ -97,7 +98,21 @@ There're a lot of things to do. Here are some of them:
- [x] Show trace information
- [ ] ...
- Metrics
- [ ] Display metrics
- [x] Metric stream
- [x] Display metric stream
- [x] Filter metrics
- [x] Show metric information
- [ ] Display basic chart of the selected metric
- [x] Gauge
- [x] Sum
- [ ] Histogram
- [ ] ExponentialHistogram
- [ ] Summary
- [ ] Metric list
- [ ] Display metric stream
- [ ] Flexible chart (query, selectable dimensions, etc.)
- [ ] Auto refresh chart
- [ ] Asynchronous chart rendering
- [ ] ...
- Logs
- [x] Display logs
Expand Down
Binary file modified docs/logs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/metrics.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/spans.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/traces.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mostynb/go-grpc-compression v1.2.3 // indirect
github.com/navidys/tvxwidgets v0.6.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mostynb/go-grpc-compression v1.2.3 h1:42/BKWMy0KEJGSdWvzqIyOZ95YcR9mLPqKctH7Uo//I=
github.com/mostynb/go-grpc-compression v1.2.3/go.mod h1:AghIxF3P57umzqM9yz795+y1Vjs47Km/Y2FE6ouQ7Lg=
github.com/navidys/tvxwidgets v0.6.0 h1:ARIXGfx4aURHMhq+LW5vIoCCDx1X/PdTF8AcUq+nWZ0=
github.com/navidys/tvxwidgets v0.6.0/go.mod h1:wd6aS2OzjZczFbg8GCaVuwkFcY1eixlT/y7Lc/YIwlg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
Expand Down
4 changes: 4 additions & 0 deletions go.work.sum
Original file line number Diff line number Diff line change
Expand Up @@ -132,17 +132,21 @@ github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBj
github.com/go-latex/latex v0.0.0-20231108140139-5c1ce85aa4ea/go.mod h1:Y7Vld91/HRbTBm7JwoI7HejdDB0u+e9AUBO9MB7yuZk=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/goccmack/gocc v0.0.0-20230228185258-2292f9e40198/go.mod h1:DTh/Y2+NbnOVVoypCCQrovMPDKUGp4yZpSbWg5D0XIM=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/onsi/ginkgo/v2 v2.16.0/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
Expand Down
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ service:
receivers: [otlp]
processors: []
exporters: [tui]
metrics:
receivers: [otlp]
processors: []
exporters: [tui]
`

configProviderSettings := otelcol.ConfigProviderSettings{
Expand Down
7 changes: 7 additions & 0 deletions tuiexporter/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/ymtdzzz/otel-tui/tuiexporter/internal/tui"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/pdata/plog"
"go.opentelemetry.io/collector/pdata/pmetric"
"go.opentelemetry.io/collector/pdata/ptrace"
)

Expand All @@ -27,6 +28,12 @@ func (e *tuiExporter) pushTraces(_ context.Context, traces ptrace.Traces) error
return nil
}

func (e *tuiExporter) pushMetrics(_ context.Context, metrics pmetric.Metrics) error {
e.app.Store().AddMetric(&metrics)

return nil
}

func (e *tuiExporter) pushLogs(_ context.Context, logs plog.Logs) error {
e.app.Store().AddLog(&logs)

Expand Down
23 changes: 22 additions & 1 deletion tuiexporter/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func NewFactory() exporter.Factory {
component.MustNewType("tui"),
createDefaultConfig,
exporter.WithTraces(createTraces, stability),
//exporter.WithMetrics(createMetrics, stability),
exporter.WithMetrics(createMetrics, stability),
exporter.WithLogs(createLogs, stability),
)
}
Expand Down Expand Up @@ -49,6 +49,27 @@ func createTraces(ctx context.Context, set exporter.Settings, cfg component.Conf
)
}

func createMetrics(ctx context.Context, set exporter.Settings, cfg component.Config) (exporter.Metrics, error) {
oCfg := cfg.(*Config)

e, err := exporters.LoadOrStore(
oCfg,
func() (*tuiExporter, error) {
return newTuiExporter(oCfg), nil
},
&set.TelemetrySettings,
)
if err != nil {
return nil, err
}

return exporterhelper.NewMetricsExporter(ctx, set, oCfg,
e.Unwrap().pushMetrics,
exporterhelper.WithStart(e.Start),
exporterhelper.WithShutdown(e.Shutdown),
)
}

func createLogs(ctx context.Context, set exporter.Settings, cfg component.Config) (exporter.Logs, error) {
oCfg := cfg.(*Config)

Expand Down
76 changes: 72 additions & 4 deletions tuiexporter/internal/telemetry/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ type TraceSpanDataMap map[string][]*SpanData
// This is used to quickly look up all spans in a trace for a service
type TraceServiceSpanDataMap map[string]map[string][]*SpanData

// TraceLogDataMap is a map of trace id to a slice of logs
// This is used to quickly look up all logs in a trace
type TraceLogDataMap map[string][]*LogData

// TraceCache is a cache of trace spans
type TraceCache struct {
spanid2span SpanDataMap
Expand Down Expand Up @@ -103,6 +99,10 @@ func (c *TraceCache) flush() {
c.tracesvc2spans = TraceServiceSpanDataMap{}
}

// TraceLogDataMap is a map of trace id to a slice of logs
// This is used to quickly look up all logs in a trace
type TraceLogDataMap map[string][]*LogData

// LogCache is a cache of logs
type LogCache struct {
traceid2logs TraceLogDataMap
Expand Down Expand Up @@ -149,3 +149,71 @@ func (c *LogCache) GetLogsByTraceID(traceID string) ([]*LogData, bool) {
func (c *LogCache) flush() {
c.traceid2logs = TraceLogDataMap{}
}

// MetricServiceMetricDataMap is a map of service name and metric name to a slice of metrics
// This is used to quickly look up datapoints in a service metric
type MetricServiceMetricDataMap map[string]map[string][]*MetricData

// MetricCache is a cache of metrics
type MetricCache struct {
svcmetric2metrics MetricServiceMetricDataMap
}

// NewMetricCache returns a new metric cache
func NewMetricCache() *MetricCache {
return &MetricCache{
svcmetric2metrics: MetricServiceMetricDataMap{},
}
}

// UpdateCache updates the cache with a new metric
func (c *MetricCache) UpdateCache(sname string, data *MetricData) {
mname := data.Metric.Name()
if sms, ok := c.svcmetric2metrics[sname]; ok {
if _, ok := sms[mname]; ok {
c.svcmetric2metrics[sname][mname] = append(c.svcmetric2metrics[sname][mname], data)
} else {
c.svcmetric2metrics[sname][mname] = []*MetricData{data}
}
} else {
c.svcmetric2metrics[sname] = map[string][]*MetricData{mname: {data}}
}
}

// DeleteCache deletes a list of metrics from the cache
func (c *MetricCache) DeleteCache(metrics []*MetricData) {
for _, m := range metrics {
sname := "N/A"
if snameattr, ok := m.ResourceMetric.Resource().Attributes().Get("service.name"); ok {
sname = snameattr.AsString()
}
mname := m.Metric.Name()
if _, ok := c.svcmetric2metrics[sname][mname]; ok {
for i, metric := range c.svcmetric2metrics[sname][mname] {
if metric == m {
c.svcmetric2metrics[sname][mname] = append(c.svcmetric2metrics[sname][mname][:i], c.svcmetric2metrics[sname][mname][i+1:]...)
if len(c.svcmetric2metrics[sname][mname]) == 0 {
delete(c.svcmetric2metrics[sname], mname)
if len(c.svcmetric2metrics[sname]) == 0 {
delete(c.svcmetric2metrics, sname)
}
}
}
}
}
}
}

// GetMetricsBySvcAndMetricName returns all metrics for a given service name and metric name
func (c *MetricCache) GetMetricsBySvcAndMetricName(sname, mname string) ([]*MetricData, bool) {
if sms, ok := c.svcmetric2metrics[sname]; ok {
if ms, ok := sms[mname]; ok {
return ms, ok
}
}
return nil, false
}

func (c *MetricCache) flush() {
c.svcmetric2metrics = MetricServiceMetricDataMap{}
}
44 changes: 44 additions & 0 deletions tuiexporter/internal/telemetry/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,47 @@ func TestGetLogsByTraceID(t *testing.T) {
})
}
}

func TestGetMetricsBySvcAndMetricName(t *testing.T) {
c := NewMetricCache()
metrics := []*MetricData{}
c.svcmetric2metrics["sname"] = map[string][]*MetricData{"mname": metrics}

tests := []struct {
name string
sname string
mname string
wantdata []*MetricData
wantok bool
}{
{
name: "service and metrics exists",
sname: "sname",
mname: "mname",
wantdata: metrics,
wantok: true,
},
{
name: "service exists but metrics does not",
sname: "sname",
mname: "non-existent-metric",
wantdata: nil,
wantok: false,
},
{
name: "service does not exist",
sname: "non-existent-sname",
mname: "mname",
wantdata: nil,
wantok: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotdata, gotok := c.GetMetricsBySvcAndMetricName(tt.sname, tt.mname)
assert.Equal(t, tt.wantdata, gotdata)
assert.Equal(t, tt.wantok, gotok)
})
}
}
Loading
Loading