@@ -33,6 +33,11 @@ import (
33
33
"time"
34
34
35
35
"github.com/gofrs/uuid/v5"
36
+ "go.elastic.co/apm/module/apmotel/v2"
37
+ "go.opentelemetry.io/otel"
38
+ "go.opentelemetry.io/otel/attribute"
39
+ sdkmetric "go.opentelemetry.io/otel/sdk/metric"
40
+ "go.opentelemetry.io/otel/sdk/metric/metricdata"
36
41
"go.uber.org/zap"
37
42
"go.uber.org/zap/exp/zapslog"
38
43
"golang.org/x/sync/errgroup"
@@ -76,6 +81,10 @@ type Beat struct {
76
81
77
82
rawConfig * config.C
78
83
newRunner NewRunnerFunc
84
+
85
+ metricReader * sdkmetric.ManualReader
86
+ meterProvider * sdkmetric.MeterProvider
87
+ metricGatherer * apmotel.Gatherer
79
88
}
80
89
81
90
// BeatParams holds parameters for NewBeat.
@@ -109,6 +118,18 @@ func NewBeat(args BeatParams) (*Beat, error) {
109
118
beatName = hostname
110
119
}
111
120
121
+ exporter , err := apmotel .NewGatherer ()
122
+ if err != nil {
123
+ return nil , err
124
+ }
125
+
126
+ metricReader := sdkmetric .NewManualReader ()
127
+ meterProvider := sdkmetric .NewMeterProvider (
128
+ sdkmetric .WithReader (exporter ),
129
+ sdkmetric .WithReader (metricReader ),
130
+ )
131
+ otel .SetMeterProvider (meterProvider )
132
+
112
133
eid := uuid .FromStringOrNil (metricreport .EphemeralID ().String ())
113
134
b := & Beat {
114
135
Beat : beat.Beat {
@@ -127,9 +148,12 @@ func NewBeat(args BeatParams) (*Beat, error) {
127
148
BeatConfig : cfg .APMServer ,
128
149
Registry : reload .NewRegistry (),
129
150
},
130
- Config : cfg ,
131
- newRunner : args .NewRunner ,
132
- rawConfig : rawConfig ,
151
+ Config : cfg ,
152
+ newRunner : args .NewRunner ,
153
+ rawConfig : rawConfig ,
154
+ metricReader : metricReader ,
155
+ meterProvider : meterProvider ,
156
+ metricGatherer : & exporter ,
133
157
}
134
158
135
159
if err := b .init (); err != nil {
@@ -374,7 +398,7 @@ func (b *Beat) Run(ctx context.Context) error {
374
398
}
375
399
376
400
if b .Manager .Enabled () {
377
- reloader , err := NewReloader (b .Info , b .Registry , b .newRunner )
401
+ reloader , err := NewReloader (b .Info , b .Registry , b .newRunner , b . meterProvider , b . metricReader , b . metricGatherer )
378
402
if err != nil {
379
403
return err
380
404
}
@@ -390,9 +414,12 @@ func (b *Beat) Run(ctx context.Context) error {
390
414
return errors .New ("no output defined, please define one under the output section" )
391
415
}
392
416
runner , err := b .newRunner (RunnerParams {
393
- Config : b .rawConfig ,
394
- Info : b .Info ,
395
- Logger : logp .NewLogger ("" ),
417
+ Config : b .rawConfig ,
418
+ Info : b .Info ,
419
+ Logger : logp .NewLogger ("" ),
420
+ MeterProvider : b .meterProvider ,
421
+ MetricReader : b .metricReader ,
422
+ MetricsGatherer : b .metricGatherer ,
396
423
})
397
424
if err != nil {
398
425
return err
@@ -410,7 +437,12 @@ func (b *Beat) Run(ctx context.Context) error {
410
437
// is then exposed through the HTTP monitoring endpoint (e.g. /info and /state)
411
438
// and/or pushed to Elasticsearch through the x-pack monitoring feature.
412
439
func (b * Beat ) registerMetrics () {
413
- // info
440
+ b .registerInfoMetrics ()
441
+ b .registerStateMetrics ()
442
+ b .registerStatsMetrics ()
443
+ }
444
+
445
+ func (b * Beat ) registerInfoMetrics () {
414
446
infoRegistry := monitoring .GetNamespace ("info" ).GetRegistry ()
415
447
monitoring .NewString (infoRegistry , "version" ).Set (b .Info .Version )
416
448
monitoring .NewString (infoRegistry , "beat" ).Set (b .Info .Beat )
@@ -436,7 +468,9 @@ func (b *Beat) registerMetrics() {
436
468
monitoring .NewString (infoRegistry , "gid" ).Set (u .Gid )
437
469
}
438
470
}()
471
+ }
439
472
473
+ func (b * Beat ) registerStateMetrics () {
440
474
stateRegistry := monitoring .GetNamespace ("state" ).GetRegistry ()
441
475
442
476
// state.service
@@ -457,6 +491,166 @@ func (b *Beat) registerMetrics() {
457
491
monitoring .NewBool (managementRegistry , "enabled" ).Set (b .Manager .Enabled ())
458
492
}
459
493
494
+ func (b * Beat ) registerStatsMetrics () {
495
+ // TODO: we should ensure all metrics are produced in the expected JSON
496
+ // hierarchy for _source compatibility.
497
+ libbeatRegistry := monitoring .Default .GetRegistry ("libbeat" )
498
+ monitoring .NewFunc (libbeatRegistry , "output" , func (_ monitoring.Mode , v monitoring.Visitor ) {
499
+ var rm metricdata.ResourceMetrics
500
+ if err := b .metricReader .Collect (context .Background (), & rm ); err != nil {
501
+ return
502
+ }
503
+ v .OnRegistryStart ()
504
+ defer v .OnRegistryFinished ()
505
+ monitoring .ReportString (v , "type" , "elasticsearch" )
506
+ for _ , sm := range rm .ScopeMetrics {
507
+ switch {
508
+ case sm .Scope .Name == "github.com/elastic/go-docappender" :
509
+ addDocappenderLibbeatOutputMetrics (context .Background (), v , sm )
510
+ }
511
+ }
512
+ })
513
+ monitoring .NewFunc (libbeatRegistry , "pipeline" , func (_ monitoring.Mode , v monitoring.Visitor ) {
514
+ var rm metricdata.ResourceMetrics
515
+ if err := b .metricReader .Collect (context .Background (), & rm ); err != nil {
516
+ return
517
+ }
518
+ v .OnRegistryStart ()
519
+ defer v .OnRegistryFinished ()
520
+ for _ , sm := range rm .ScopeMetrics {
521
+ switch {
522
+ case sm .Scope .Name == "github.com/elastic/go-docappender" :
523
+ addDocappenderLibbeatPipelineMetrics (context .Background (), v , sm )
524
+ }
525
+ }
526
+ })
527
+ monitoring .NewFunc (monitoring .Default , "output.elasticsearch" , func (_ monitoring.Mode , v monitoring.Visitor ) {
528
+ var rm metricdata.ResourceMetrics
529
+ if err := b .metricReader .Collect (context .Background (), & rm ); err != nil {
530
+ return
531
+ }
532
+ v .OnRegistryStart ()
533
+ defer v .OnRegistryFinished ()
534
+ for _ , sm := range rm .ScopeMetrics {
535
+ switch {
536
+ case sm .Scope .Name == "github.com/elastic/go-docappender" :
537
+ addDocappenderOutputElasticsearchMetrics (context .Background (), v , sm )
538
+ }
539
+ }
540
+ })
541
+ }
542
+
543
+ // getScalarInt64 returns a single-value, dimensionless
544
+ // gauge or counter integer value, or (0, false) if the
545
+ // data does not match these constraints.
546
+ func getScalarInt64 (data metricdata.Aggregation ) (int64 , bool ) {
547
+ switch data := data .(type ) {
548
+ case metricdata.Sum [int64 ]:
549
+ if len (data .DataPoints ) != 1 || data .DataPoints [0 ].Attributes .Len () != 0 {
550
+ break
551
+ }
552
+ return data .DataPoints [0 ].Value , true
553
+ case metricdata.Gauge [int64 ]:
554
+ if len (data .DataPoints ) != 1 || data .DataPoints [0 ].Attributes .Len () != 0 {
555
+ break
556
+ }
557
+ return data .DataPoints [0 ].Value , true
558
+ }
559
+ return 0 , false
560
+ }
561
+
562
+ func addAPMServerMetrics (v monitoring.Visitor , sm metricdata.ScopeMetrics ) {
563
+ for _ , m := range sm .Metrics {
564
+ if suffix , ok := strings .CutPrefix (m .Name , "apm-server." ); ok {
565
+ if value , ok := getScalarInt64 (m .Data ); ok {
566
+ monitoring .ReportInt (v , suffix , value )
567
+ }
568
+ }
569
+ }
570
+ }
571
+
572
+ // Adapt go-docappender's OTel metrics to beats stack monitoring metrics,
573
+ // with a mixture of libbeat-specific and apm-server specific metric names.
574
+ func addDocappenderLibbeatOutputMetrics (ctx context.Context , v monitoring.Visitor , sm metricdata.ScopeMetrics ) {
575
+ for _ , m := range sm .Metrics {
576
+ switch m .Name {
577
+ case "elasticsearch.events.processed" :
578
+ var acked , toomany , failed int64
579
+ data , _ := m .Data .(metricdata.Sum [int64 ])
580
+ for _ , dp := range data .DataPoints {
581
+ status , ok := dp .Attributes .Value (attribute .Key ("status" ))
582
+ if ! ok {
583
+ continue
584
+ }
585
+ switch status .AsString () {
586
+ case "Success" :
587
+ acked ++
588
+ case "TooMany" :
589
+ toomany ++
590
+ fallthrough
591
+ default :
592
+ failed ++
593
+ }
594
+ }
595
+ monitoring .ReportInt (v , "events.acked" , acked )
596
+ monitoring .ReportInt (v , "events.failed" , failed )
597
+ monitoring .ReportInt (v , "events.toomany" , toomany )
598
+ case "elasticsearch.events.count" :
599
+ if value , ok := getScalarInt64 (m .Data ); ok {
600
+ monitoring .ReportInt (v , "events.total" , value )
601
+ }
602
+ case "elasticsearch.events.queued" :
603
+ if value , ok := getScalarInt64 (m .Data ); ok {
604
+ monitoring .ReportInt (v , "events.active" , value )
605
+ }
606
+ case "elasticsearch.flushed.bytes" :
607
+ if value , ok := getScalarInt64 (m .Data ); ok {
608
+ monitoring .ReportInt (v , "write.bytes" , value )
609
+ }
610
+ case "elasticsearch.bulk_requests.count" :
611
+ if value , ok := getScalarInt64 (m .Data ); ok {
612
+ monitoring .ReportInt (v , "events.batches" , value )
613
+ }
614
+ }
615
+ }
616
+ }
617
+
618
+ func addDocappenderLibbeatPipelineMetrics (ctx context.Context , v monitoring.Visitor , sm metricdata.ScopeMetrics ) {
619
+ for _ , m := range sm .Metrics {
620
+ switch m .Name {
621
+ case "elasticsearch.events.count" :
622
+ if value , ok := getScalarInt64 (m .Data ); ok {
623
+ monitoring .ReportInt (v , "events.total" , value )
624
+ }
625
+ }
626
+ }
627
+ }
628
+
629
+ // Add non-libbeat Elasticsearch output metrics under "output.elasticsearch".
630
+ func addDocappenderOutputElasticsearchMetrics (ctx context.Context , v monitoring.Visitor , sm metricdata.ScopeMetrics ) {
631
+ for _ , m := range sm .Metrics {
632
+ switch m .Name {
633
+ case "elasticsearch.bulk_requests.count" :
634
+ if value , ok := getScalarInt64 (m .Data ); ok {
635
+ monitoring .ReportInt (v , "bulk_requests.completed" , value )
636
+ }
637
+ case "elasticsearch.bulk_requests.available" :
638
+ if value , ok := getScalarInt64 (m .Data ); ok {
639
+ monitoring .ReportInt (v , "bulk_requests.available" , value )
640
+ }
641
+ case "elasticsearch.indexers.created" :
642
+ if value , ok := getScalarInt64 (m .Data ); ok {
643
+ monitoring .ReportInt (v , "indexers.created" , value )
644
+ }
645
+ case "elasticsearch.indexers.destroyed" :
646
+ if value , ok := getScalarInt64 (m .Data ); ok {
647
+ monitoring .ReportInt (v , "indexers.destroyed" , value )
648
+ }
649
+ // TODO output.elasticsearch.indexers.active (created - destroyed?)
650
+ }
651
+ }
652
+ }
653
+
460
654
// registerElasticsearchVerfication registers a global callback to make sure
461
655
// the Elasticsearch instance we are connecting to has a valid license, and is
462
656
// at least on the same version as APM Server.
0 commit comments