[chore] Add Go runtime and host metrics (#4137)

Daenney is a dummy and forgot to add these when he revamped the OTEL stuff.

Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4137
Co-authored-by: Daenney <daenney@noreply.codeberg.org>
Co-committed-by: Daenney <daenney@noreply.codeberg.org>
This commit is contained in:
Daenney
2025-05-06 08:18:05 +00:00
committed by tobi
parent 4a6b357501
commit 90a5425fe9
14 changed files with 1124 additions and 0 deletions

1
go.mod
View File

@@ -73,6 +73,7 @@ require (
github.com/wagslane/go-password-validator v0.3.0
github.com/yuin/goldmark v1.7.11
go.opentelemetry.io/contrib/exporters/autoexport v0.60.0
go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0
go.opentelemetry.io/otel v1.35.0
go.opentelemetry.io/otel/metric v1.35.0
go.opentelemetry.io/otel/sdk v1.35.0

2
go.sum generated
View File

@@ -495,6 +495,8 @@ go.opentelemetry.io/contrib/bridges/prometheus v0.60.0 h1:x7sPooQCwSg27SjtQee8Gy
go.opentelemetry.io/contrib/bridges/prometheus v0.60.0/go.mod h1:4K5UXgiHxV484efGs42ejD7E2J/sIlepYgdGoPXe7hE=
go.opentelemetry.io/contrib/exporters/autoexport v0.60.0 h1:GuQXpvSXNjpswpweIem84U9BNauqHHi2w1GtNAalvpM=
go.opentelemetry.io/contrib/exporters/autoexport v0.60.0/go.mod h1:CkmxekdHco4d7thFJNPQ7Mby4jMBgZUclnrxT4e+ryk=
go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0 h1:0NgN/3SYkqYJ9NBlDfl/2lzVlwos/YQLvi8sUrzJRBE=
go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0/go.mod h1:oxpUfhTkhgQaYIjtBt3T3w135dLoxq//qo3WPlPIKkE=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0 h1:HMUytBT3uGhPKYY/u/G5MR9itrlSO2SMOsSD3Tk3k7A=

View File

@@ -29,6 +29,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/technologize/otel-go-contrib/otelginmetrics"
"go.opentelemetry.io/contrib/exporters/autoexport"
"go.opentelemetry.io/contrib/instrumentation/runtime"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric"
sdk "go.opentelemetry.io/otel/sdk/metric"
@@ -59,6 +60,12 @@ func InitializeMetrics(ctx context.Context, db db.DB) error {
otel.SetMeterProvider(meterProvider)
if err := runtime.Start(
runtime.WithMeterProvider(meterProvider),
); err != nil {
return err
}
meter := meterProvider.Meter(serviceName)
thisInstance := config.GetHost()

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,34 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
// Package runtime implements the conventional runtime metrics specified by OpenTelemetry.
//
// The metric events produced are:
//
// runtime.go.cgo.calls - Number of cgo calls made by the current process
// runtime.go.gc.count - Number of completed garbage collection cycles
// runtime.go.gc.pause_ns (ns) Amount of nanoseconds in GC stop-the-world pauses
// runtime.go.gc.pause_total_ns (ns) Cumulative nanoseconds in GC stop-the-world pauses since the program started
// runtime.go.goroutines - Number of goroutines that currently exist
// runtime.go.lookups - Number of pointer lookups performed by the runtime
// runtime.go.mem.heap_alloc (bytes) Bytes of allocated heap objects
// runtime.go.mem.heap_idle (bytes) Bytes in idle (unused) spans
// runtime.go.mem.heap_inuse (bytes) Bytes in in-use spans
// runtime.go.mem.heap_objects - Number of allocated heap objects
// runtime.go.mem.heap_released (bytes) Bytes of idle spans whose physical memory has been returned to the OS
// runtime.go.mem.heap_sys (bytes) Bytes of heap memory obtained from the OS
// runtime.go.mem.live_objects - Number of live objects is the number of cumulative Mallocs - Frees
// runtime.uptime (ms) Milliseconds since application was initialized
//
// When the OTEL_GO_X_DEPRECATED_RUNTIME_METRICS environment variable is set to
// false, the metrics produced are:
//
// go.memory.used By Memory used by the Go runtime.
// go.memory.limit By Go runtime memory limit configured by the user, if a limit exists.
// go.memory.allocated By Memory allocated to the heap by the application.
// go.memory.allocations {allocation} Count of allocations to the heap by the application.
// go.memory.gc.goal By Heap size target for the end of the GC cycle.
// go.goroutine.count {goroutine} Count of live goroutines.
// go.processor.limit {thread} The number of OS threads that can execute user-level Go code simultaneously.
// go.config.gogc % Heap size target percentage configured by the user, otherwise 100.
package runtime // import "go.opentelemetry.io/contrib/instrumentation/runtime"

View File

@@ -0,0 +1,22 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
// Package deprecatedruntime implements the deprecated runtime metrics for OpenTelemetry.
//
// The metric events produced are:
//
// runtime.go.cgo.calls - Number of cgo calls made by the current process
// runtime.go.gc.count - Number of completed garbage collection cycles
// runtime.go.gc.pause_ns (ns) Amount of nanoseconds in GC stop-the-world pauses
// runtime.go.gc.pause_total_ns (ns) Cumulative nanoseconds in GC stop-the-world pauses since the program started
// runtime.go.goroutines - Number of goroutines that currently exist
// runtime.go.lookups - Number of pointer lookups performed by the runtime
// runtime.go.mem.heap_alloc (bytes) Bytes of allocated heap objects
// runtime.go.mem.heap_idle (bytes) Bytes in idle (unused) spans
// runtime.go.mem.heap_inuse (bytes) Bytes in in-use spans
// runtime.go.mem.heap_objects - Number of allocated heap objects
// runtime.go.mem.heap_released (bytes) Bytes of idle spans whose physical memory has been returned to the OS
// runtime.go.mem.heap_sys (bytes) Bytes of heap memory obtained from the OS
// runtime.go.mem.live_objects - Number of live objects is the number of cumulative Mallocs - Frees
// runtime.uptime (ms) Milliseconds since application was initialized
package deprecatedruntime // import "go.opentelemetry.io/contrib/instrumentation/runtime/internal/deprecatedruntime"

View File

@@ -0,0 +1,296 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package deprecatedruntime // import "go.opentelemetry.io/contrib/instrumentation/runtime/internal/deprecatedruntime"
import (
"context"
"math"
goruntime "runtime"
"sync"
"time"
"go.opentelemetry.io/otel/metric"
)
// Runtime reports the work-in-progress conventional runtime metrics specified by OpenTelemetry.
type runtime struct {
minimumReadMemStatsInterval time.Duration
meter metric.Meter
}
// Start initializes reporting of runtime metrics using the supplied config.
func Start(meter metric.Meter, minimumReadMemStatsInterval time.Duration) error {
r := &runtime{
meter: meter,
minimumReadMemStatsInterval: minimumReadMemStatsInterval,
}
return r.register()
}
func (r *runtime) register() error {
startTime := time.Now()
uptime, err := r.meter.Int64ObservableCounter(
"runtime.uptime",
metric.WithUnit("ms"),
metric.WithDescription("Milliseconds since application was initialized"),
)
if err != nil {
return err
}
goroutines, err := r.meter.Int64ObservableUpDownCounter(
"process.runtime.go.goroutines",
metric.WithDescription("Number of goroutines that currently exist"),
)
if err != nil {
return err
}
cgoCalls, err := r.meter.Int64ObservableUpDownCounter(
"process.runtime.go.cgo.calls",
metric.WithDescription("Number of cgo calls made by the current process"),
)
if err != nil {
return err
}
_, err = r.meter.RegisterCallback(
func(ctx context.Context, o metric.Observer) error {
o.ObserveInt64(uptime, time.Since(startTime).Milliseconds())
o.ObserveInt64(goroutines, int64(goruntime.NumGoroutine()))
o.ObserveInt64(cgoCalls, goruntime.NumCgoCall())
return nil
},
uptime,
goroutines,
cgoCalls,
)
if err != nil {
return err
}
return r.registerMemStats()
}
func (r *runtime) registerMemStats() error {
var (
err error
heapAlloc metric.Int64ObservableUpDownCounter
heapIdle metric.Int64ObservableUpDownCounter
heapInuse metric.Int64ObservableUpDownCounter
heapObjects metric.Int64ObservableUpDownCounter
heapReleased metric.Int64ObservableUpDownCounter
heapSys metric.Int64ObservableUpDownCounter
liveObjects metric.Int64ObservableUpDownCounter
// TODO: is ptrLookups useful? I've not seen a value
// other than zero.
ptrLookups metric.Int64ObservableCounter
gcCount metric.Int64ObservableCounter
pauseTotalNs metric.Int64ObservableCounter
gcPauseNs metric.Int64Histogram
lastNumGC uint32
lastMemStats time.Time
memStats goruntime.MemStats
// lock prevents a race between batch observer and instrument registration.
lock sync.Mutex
)
lock.Lock()
defer lock.Unlock()
if heapAlloc, err = r.meter.Int64ObservableUpDownCounter(
"process.runtime.go.mem.heap_alloc",
metric.WithUnit("By"),
metric.WithDescription("Bytes of allocated heap objects"),
); err != nil {
return err
}
if heapIdle, err = r.meter.Int64ObservableUpDownCounter(
"process.runtime.go.mem.heap_idle",
metric.WithUnit("By"),
metric.WithDescription("Bytes in idle (unused) spans"),
); err != nil {
return err
}
if heapInuse, err = r.meter.Int64ObservableUpDownCounter(
"process.runtime.go.mem.heap_inuse",
metric.WithUnit("By"),
metric.WithDescription("Bytes in in-use spans"),
); err != nil {
return err
}
if heapObjects, err = r.meter.Int64ObservableUpDownCounter(
"process.runtime.go.mem.heap_objects",
metric.WithDescription("Number of allocated heap objects"),
); err != nil {
return err
}
// FYI see https://github.com/golang/go/issues/32284 to help
// understand the meaning of this value.
if heapReleased, err = r.meter.Int64ObservableUpDownCounter(
"process.runtime.go.mem.heap_released",
metric.WithUnit("By"),
metric.WithDescription("Bytes of idle spans whose physical memory has been returned to the OS"),
); err != nil {
return err
}
if heapSys, err = r.meter.Int64ObservableUpDownCounter(
"process.runtime.go.mem.heap_sys",
metric.WithUnit("By"),
metric.WithDescription("Bytes of heap memory obtained from the OS"),
); err != nil {
return err
}
if ptrLookups, err = r.meter.Int64ObservableCounter(
"process.runtime.go.mem.lookups",
metric.WithDescription("Number of pointer lookups performed by the runtime"),
); err != nil {
return err
}
if liveObjects, err = r.meter.Int64ObservableUpDownCounter(
"process.runtime.go.mem.live_objects",
metric.WithDescription("Number of live objects is the number of cumulative Mallocs - Frees"),
); err != nil {
return err
}
if gcCount, err = r.meter.Int64ObservableCounter(
"process.runtime.go.gc.count",
metric.WithDescription("Number of completed garbage collection cycles"),
); err != nil {
return err
}
// Note that the following could be derived as a sum of
// individual pauses, but we may lose individual pauses if the
// observation interval is too slow.
if pauseTotalNs, err = r.meter.Int64ObservableCounter(
"process.runtime.go.gc.pause_total_ns",
// TODO: nanoseconds units
metric.WithDescription("Cumulative nanoseconds in GC stop-the-world pauses since the program started"),
); err != nil {
return err
}
if gcPauseNs, err = r.meter.Int64Histogram(
"process.runtime.go.gc.pause_ns",
// TODO: nanoseconds units
metric.WithDescription("Amount of nanoseconds in GC stop-the-world pauses"),
); err != nil {
return err
}
_, err = r.meter.RegisterCallback(
func(ctx context.Context, o metric.Observer) error {
lock.Lock()
defer lock.Unlock()
now := time.Now()
if now.Sub(lastMemStats) >= r.minimumReadMemStatsInterval {
goruntime.ReadMemStats(&memStats)
lastMemStats = now
}
o.ObserveInt64(heapAlloc, clampUint64(memStats.HeapAlloc))
o.ObserveInt64(heapIdle, clampUint64(memStats.HeapIdle))
o.ObserveInt64(heapInuse, clampUint64(memStats.HeapInuse))
o.ObserveInt64(heapObjects, clampUint64(memStats.HeapObjects))
o.ObserveInt64(heapReleased, clampUint64(memStats.HeapReleased))
o.ObserveInt64(heapSys, clampUint64(memStats.HeapSys))
o.ObserveInt64(liveObjects, clampUint64(memStats.Mallocs-memStats.Frees))
o.ObserveInt64(ptrLookups, clampUint64(memStats.Lookups))
o.ObserveInt64(gcCount, int64(memStats.NumGC))
o.ObserveInt64(pauseTotalNs, clampUint64(memStats.PauseTotalNs))
computeGCPauses(ctx, gcPauseNs, memStats.PauseNs[:], lastNumGC, memStats.NumGC)
lastNumGC = memStats.NumGC
return nil
},
heapAlloc,
heapIdle,
heapInuse,
heapObjects,
heapReleased,
heapSys,
liveObjects,
ptrLookups,
gcCount,
pauseTotalNs,
)
if err != nil {
return err
}
return nil
}
func clampUint64(v uint64) int64 {
if v > math.MaxInt64 {
return math.MaxInt64
}
return int64(v) // nolint: gosec // Overflow checked above.
}
func computeGCPauses(
ctx context.Context,
recorder metric.Int64Histogram,
circular []uint64,
lastNumGC, currentNumGC uint32,
) {
delta := int(int64(currentNumGC) - int64(lastNumGC))
if delta == 0 {
return
}
if delta >= len(circular) {
// There were > 256 collections, some may have been lost.
recordGCPauses(ctx, recorder, circular)
return
}
n := len(circular)
if n < 0 {
// Only the case in error situations.
return
}
length := uint64(n) // nolint: gosec // n >= 0
i := uint64(lastNumGC) % length
j := uint64(currentNumGC) % length
if j < i { // wrap around the circular buffer
recordGCPauses(ctx, recorder, circular[i:])
recordGCPauses(ctx, recorder, circular[:j])
return
}
recordGCPauses(ctx, recorder, circular[i:j])
}
func recordGCPauses(
ctx context.Context,
recorder metric.Int64Histogram,
pauses []uint64,
) {
for _, pause := range pauses {
recorder.Record(ctx, clampUint64(pause))
}
}

View File

@@ -0,0 +1,38 @@
# Feature Gates
The runtime package contains a feature gate used to ease the migration
from the [previous runtime metrics conventions] to the new [OpenTelemetry Go
Runtime conventions].
Note that the new runtime metrics conventions are still experimental, and may
change in backwards incompatible ways as feedback is applied.
## Features
- [Include Deprecated Metrics](#include-deprecated-metrics)
### Include Deprecated Metrics
Once new experimental runtime metrics are added, they will be produced
**in addition to** the existing runtime metrics. Users that migrate right away
can disable the old runtime metrics:
```console
export OTEL_GO_X_DEPRECATED_RUNTIME_METRICS=false
```
In a later release, the deprecated runtime metrics will stop being produced by
default. To temporarily re-enable the deprecated metrics:
```console
export OTEL_GO_X_DEPRECATED_RUNTIME_METRICS=true
```
After two additional releases, the deprecated runtime metrics will be removed,
and setting the environment variable will no longer have any effect.
The value set must be the case-insensitive string of `"true"` to enable the
feature, and `"false"` to disable the feature. All other values are ignored.
[previous runtime metrics conventions]: https://pkg.go.dev/go.opentelemetry.io/contrib/instrumentation/runtime@v0.52.0
[OpenTelemetry Go Runtime conventions]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/runtime/go-metrics.md

View File

@@ -0,0 +1,53 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
// Package x contains support for OTel runtime instrumentation experimental features.
//
// This package should only be used for features defined in the specification.
// It should not be used for experiments or new project ideas.
package x // import "go.opentelemetry.io/contrib/instrumentation/runtime/internal/x"
import (
"os"
"strings"
)
// DeprecatedRuntimeMetrics is an experimental feature flag that defines if the deprecated
// runtime metrics should be produced. During development of the new
// conventions, it is enabled by default.
//
// To disable this feature set the OTEL_GO_X_DEPRECATED_RUNTIME_METRICS environment variable
// to the case-insensitive string value of "false" (i.e. "False" and "FALSE"
// will also enable this).
var DeprecatedRuntimeMetrics = newFeature("DEPRECATED_RUNTIME_METRICS", true)
// BoolFeature is an experimental feature control flag. It provides a uniform way
// to interact with these feature flags and parse their values.
type BoolFeature struct {
key string
defaultVal bool
}
func newFeature(suffix string, defaultVal bool) BoolFeature {
const envKeyRoot = "OTEL_GO_X_"
return BoolFeature{
key: envKeyRoot + suffix,
defaultVal: defaultVal,
}
}
// Key returns the environment variable key that needs to be set to enable the
// feature.
func (f BoolFeature) Key() string { return f.key }
// Enabled returns if the feature is enabled.
func (f BoolFeature) Enabled() bool {
v := os.Getenv(f.key)
if strings.ToLower(v) == "false" {
return false
}
if strings.ToLower(v) == "true" {
return true
}
return f.defaultVal
}

View File

@@ -0,0 +1,99 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package runtime // import "go.opentelemetry.io/contrib/instrumentation/runtime"
import (
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric"
)
// config contains optional settings for reporting runtime metrics.
type config struct {
// MinimumReadMemStatsInterval sets the minimum interval
// between calls to runtime.ReadMemStats(). Negative values
// are ignored.
MinimumReadMemStatsInterval time.Duration
// MeterProvider sets the metric.MeterProvider. If nil, the global
// Provider will be used.
MeterProvider metric.MeterProvider
}
// Option supports configuring optional settings for runtime metrics.
type Option interface {
apply(*config)
}
// ProducerOption supports configuring optional settings for runtime metrics using a
// metric producer in addition to standard instrumentation.
type ProducerOption interface {
Option
applyProducer(*config)
}
// DefaultMinimumReadMemStatsInterval is the default minimum interval
// between calls to runtime.ReadMemStats(). Use the
// WithMinimumReadMemStatsInterval() option to modify this setting in
// Start().
const DefaultMinimumReadMemStatsInterval time.Duration = 15 * time.Second
// WithMinimumReadMemStatsInterval sets a minimum interval between calls to
// runtime.ReadMemStats(), which is a relatively expensive call to make
// frequently. This setting is ignored when `d` is negative.
func WithMinimumReadMemStatsInterval(d time.Duration) Option {
return minimumReadMemStatsIntervalOption(d)
}
type minimumReadMemStatsIntervalOption time.Duration
func (o minimumReadMemStatsIntervalOption) apply(c *config) {
if o >= 0 {
c.MinimumReadMemStatsInterval = time.Duration(o)
}
}
func (o minimumReadMemStatsIntervalOption) applyProducer(c *config) { o.apply(c) }
// WithMeterProvider sets the Metric implementation to use for
// reporting. If this option is not used, the global metric.MeterProvider
// will be used. `provider` must be non-nil.
func WithMeterProvider(provider metric.MeterProvider) Option {
return metricProviderOption{provider}
}
type metricProviderOption struct{ metric.MeterProvider }
func (o metricProviderOption) apply(c *config) {
if o.MeterProvider != nil {
c.MeterProvider = o.MeterProvider
}
}
// newConfig computes a config from the supplied Options.
func newConfig(opts ...Option) config {
c := config{
MeterProvider: otel.GetMeterProvider(),
}
for _, opt := range opts {
opt.apply(&c)
}
if c.MinimumReadMemStatsInterval <= 0 {
c.MinimumReadMemStatsInterval = DefaultMinimumReadMemStatsInterval
}
return c
}
// newConfig computes a config from the supplied ProducerOptions.
func newProducerConfig(opts ...ProducerOption) config {
c := config{}
for _, opt := range opts {
opt.applyProducer(&c)
}
if c.MinimumReadMemStatsInterval <= 0 {
c.MinimumReadMemStatsInterval = DefaultMinimumReadMemStatsInterval
}
return c
}

View File

@@ -0,0 +1,120 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package runtime // import "go.opentelemetry.io/contrib/instrumentation/runtime"
import (
"context"
"errors"
"math"
"runtime/metrics"
"sync"
"time"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/instrumentation"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
)
var startTime time.Time
func init() {
startTime = time.Now()
}
var histogramMetrics = []string{goSchedLatencies}
// Producer is a metric.Producer, which provides precomputed histogram metrics from the go runtime.
type Producer struct {
lock sync.Mutex
collector *goCollector
}
var _ metric.Producer = (*Producer)(nil)
// NewProducer creates a Producer which provides precomputed histogram metrics from the go runtime.
func NewProducer(opts ...ProducerOption) *Producer {
c := newProducerConfig(opts...)
return &Producer{
collector: newCollector(c.MinimumReadMemStatsInterval, histogramMetrics),
}
}
// Produce returns precomputed histogram metrics from the go runtime, or an error if unsuccessful.
func (p *Producer) Produce(context.Context) ([]metricdata.ScopeMetrics, error) {
p.lock.Lock()
p.collector.refresh()
schedHist := p.collector.getHistogram(goSchedLatencies)
p.lock.Unlock()
// Use the last collection time (which may or may not be now) for the timestamp.
histDp := convertRuntimeHistogram(schedHist, p.collector.lastCollect)
if len(histDp) == 0 {
return nil, errors.New("unable to obtain go.schedule.duration metric from the runtime")
}
return []metricdata.ScopeMetrics{
{
Scope: instrumentation.Scope{
Name: ScopeName,
Version: Version(),
},
Metrics: []metricdata.Metrics{
{
Name: "go.schedule.duration",
Description: "The time goroutines have spent in the scheduler in a runnable state before actually running.",
Unit: "s",
Data: metricdata.Histogram[float64]{
Temporality: metricdata.CumulativeTemporality,
DataPoints: histDp,
},
},
},
},
}, nil
}
var emptySet = attribute.EmptySet()
func convertRuntimeHistogram(runtimeHist *metrics.Float64Histogram, ts time.Time) []metricdata.HistogramDataPoint[float64] {
if runtimeHist == nil {
return nil
}
bounds := runtimeHist.Buckets
counts := runtimeHist.Counts
if len(bounds) < 2 {
// runtime histograms are guaranteed to have at least two bucket boundaries.
return nil
}
// trim the first bucket since it is a lower bound. OTel histogram boundaries only have an upper bound.
bounds = bounds[1:]
if bounds[len(bounds)-1] == math.Inf(1) {
// trim the last bucket if it is +Inf, since the +Inf boundary is implicit in OTel.
bounds = bounds[:len(bounds)-1]
} else {
// if the last bucket is not +Inf, append an extra zero count since
// the implicit +Inf bucket won't have any observations.
counts = append(counts, 0)
}
count := uint64(0)
sum := float64(0)
for i, c := range counts {
count += c
// This computed sum is an underestimate, since it assumes each
// observation happens at the bucket's lower bound.
if i > 0 && count != 0 {
sum += bounds[i-1] * float64(count)
}
}
return []metricdata.HistogramDataPoint[float64]{
{
StartTime: startTime,
Count: count,
Sum: sum,
Time: ts,
Bounds: bounds,
BucketCounts: counts,
Attributes: *emptySet,
},
}
}

View File

@@ -0,0 +1,229 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package runtime // import "go.opentelemetry.io/contrib/instrumentation/runtime"
import (
"context"
"math"
"runtime/metrics"
"sync"
"time"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/contrib/instrumentation/runtime/internal/deprecatedruntime"
"go.opentelemetry.io/contrib/instrumentation/runtime/internal/x"
)
// ScopeName is the instrumentation scope name.
const ScopeName = "go.opentelemetry.io/contrib/instrumentation/runtime"
const (
goTotalMemory = "/memory/classes/total:bytes"
goMemoryReleased = "/memory/classes/heap/released:bytes"
goHeapMemory = "/memory/classes/heap/stacks:bytes"
goMemoryLimit = "/gc/gomemlimit:bytes"
goMemoryAllocated = "/gc/heap/allocs:bytes"
goMemoryAllocations = "/gc/heap/allocs:objects"
goMemoryGoal = "/gc/heap/goal:bytes"
goGoroutines = "/sched/goroutines:goroutines"
goMaxProcs = "/sched/gomaxprocs:threads"
goConfigGC = "/gc/gogc:percent"
goSchedLatencies = "/sched/latencies:seconds"
)
// Start initializes reporting of runtime metrics using the supplied config.
// For goroutine scheduling metrics, additionally see [NewProducer].
func Start(opts ...Option) error {
c := newConfig(opts...)
meter := c.MeterProvider.Meter(
ScopeName,
metric.WithInstrumentationVersion(Version()),
)
if x.DeprecatedRuntimeMetrics.Enabled() {
return deprecatedruntime.Start(meter, c.MinimumReadMemStatsInterval)
}
memoryUsedInstrument, err := meter.Int64ObservableUpDownCounter(
"go.memory.used",
metric.WithUnit("By"),
metric.WithDescription("Memory used by the Go runtime."),
)
if err != nil {
return err
}
memoryLimitInstrument, err := meter.Int64ObservableUpDownCounter(
"go.memory.limit",
metric.WithUnit("By"),
metric.WithDescription("Go runtime memory limit configured by the user, if a limit exists."),
)
if err != nil {
return err
}
memoryAllocatedInstrument, err := meter.Int64ObservableCounter(
"go.memory.allocated",
metric.WithUnit("By"),
metric.WithDescription("Memory allocated to the heap by the application."),
)
if err != nil {
return err
}
memoryAllocationsInstrument, err := meter.Int64ObservableCounter(
"go.memory.allocations",
metric.WithUnit("{allocation}"),
metric.WithDescription("Count of allocations to the heap by the application."),
)
if err != nil {
return err
}
memoryGCGoalInstrument, err := meter.Int64ObservableUpDownCounter(
"go.memory.gc.goal",
metric.WithUnit("By"),
metric.WithDescription("Heap size target for the end of the GC cycle."),
)
if err != nil {
return err
}
goroutineCountInstrument, err := meter.Int64ObservableUpDownCounter(
"go.goroutine.count",
metric.WithUnit("{goroutine}"),
metric.WithDescription("Count of live goroutines."),
)
if err != nil {
return err
}
processorLimitInstrument, err := meter.Int64ObservableUpDownCounter(
"go.processor.limit",
metric.WithUnit("{thread}"),
metric.WithDescription("The number of OS threads that can execute user-level Go code simultaneously."),
)
if err != nil {
return err
}
gogcConfigInstrument, err := meter.Int64ObservableUpDownCounter(
"go.config.gogc",
metric.WithUnit("%"),
metric.WithDescription("Heap size target percentage configured by the user, otherwise 100."),
)
if err != nil {
return err
}
otherMemoryOpt := metric.WithAttributeSet(
attribute.NewSet(attribute.String("go.memory.type", "other")),
)
stackMemoryOpt := metric.WithAttributeSet(
attribute.NewSet(attribute.String("go.memory.type", "stack")),
)
collector := newCollector(c.MinimumReadMemStatsInterval, runtimeMetrics)
var lock sync.Mutex
_, err = meter.RegisterCallback(
func(ctx context.Context, o metric.Observer) error {
lock.Lock()
defer lock.Unlock()
collector.refresh()
stackMemory := collector.getInt(goHeapMemory)
o.ObserveInt64(memoryUsedInstrument, stackMemory, stackMemoryOpt)
totalMemory := collector.getInt(goTotalMemory) - collector.getInt(goMemoryReleased)
otherMemory := totalMemory - stackMemory
o.ObserveInt64(memoryUsedInstrument, otherMemory, otherMemoryOpt)
// Only observe the limit metric if a limit exists
if limit := collector.getInt(goMemoryLimit); limit != math.MaxInt64 {
o.ObserveInt64(memoryLimitInstrument, limit)
}
o.ObserveInt64(memoryAllocatedInstrument, collector.getInt(goMemoryAllocated))
o.ObserveInt64(memoryAllocationsInstrument, collector.getInt(goMemoryAllocations))
o.ObserveInt64(memoryGCGoalInstrument, collector.getInt(goMemoryGoal))
o.ObserveInt64(goroutineCountInstrument, collector.getInt(goGoroutines))
o.ObserveInt64(processorLimitInstrument, collector.getInt(goMaxProcs))
o.ObserveInt64(gogcConfigInstrument, collector.getInt(goConfigGC))
return nil
},
memoryUsedInstrument,
memoryLimitInstrument,
memoryAllocatedInstrument,
memoryAllocationsInstrument,
memoryGCGoalInstrument,
goroutineCountInstrument,
processorLimitInstrument,
gogcConfigInstrument,
)
if err != nil {
return err
}
return nil
}
// These are the metrics we actually fetch from the go runtime.
var runtimeMetrics = []string{
goTotalMemory,
goMemoryReleased,
goHeapMemory,
goMemoryLimit,
goMemoryAllocated,
goMemoryAllocations,
goMemoryGoal,
goGoroutines,
goMaxProcs,
goConfigGC,
}
type goCollector struct {
// now is used to replace the implementation of time.Now for testing
now func() time.Time
// lastCollect tracks the last time metrics were refreshed
lastCollect time.Time
// minimumInterval is the minimum amount of time between calls to metrics.Read
minimumInterval time.Duration
// sampleBuffer is populated by runtime/metrics
sampleBuffer []metrics.Sample
// sampleMap allows us to easily get the value of a single metric
sampleMap map[string]*metrics.Sample
}
func newCollector(minimumInterval time.Duration, metricNames []string) *goCollector {
g := &goCollector{
sampleBuffer: make([]metrics.Sample, 0, len(metricNames)),
sampleMap: make(map[string]*metrics.Sample, len(metricNames)),
minimumInterval: minimumInterval,
now: time.Now,
}
for _, metricName := range metricNames {
g.sampleBuffer = append(g.sampleBuffer, metrics.Sample{Name: metricName})
// sampleMap references a position in the sampleBuffer slice. If an
// element is appended to sampleBuffer, it must be added to sampleMap
// for the sample to be accessible in sampleMap.
g.sampleMap[metricName] = &g.sampleBuffer[len(g.sampleBuffer)-1]
}
return g
}
func (g *goCollector) refresh() {
now := g.now()
if now.Sub(g.lastCollect) < g.minimumInterval {
// refresh was invoked more frequently than allowed by the minimum
// interval. Do nothing.
return
}
metrics.Read(g.sampleBuffer)
g.lastCollect = now
}
func (g *goCollector) getInt(name string) int64 {
if s, ok := g.sampleMap[name]; ok && s.Value.Kind() == metrics.KindUint64 {
v := s.Value.Uint64()
if v > math.MaxInt64 {
return math.MaxInt64
}
return int64(v) // nolint: gosec // Overflow checked above.
}
return 0
}
func (g *goCollector) getHistogram(name string) *metrics.Float64Histogram {
if s, ok := g.sampleMap[name]; ok && s.Value.Kind() == metrics.KindFloat64Histogram {
return s.Value.Float64Histogram()
}
return nil
}

View File

@@ -0,0 +1,17 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package runtime // import "go.opentelemetry.io/contrib/instrumentation/runtime"
// Version is the current release version of the runtime instrumentation.
func Version() string {
return "0.60.0"
// This string is updated by the pre_release.sh script during release
}
// SemVersion is the semantic version to be supplied to tracer/meter creation.
//
// Deprecated: Use [Version] instead.
func SemVersion() string {
return Version()
}

5
vendor/modules.txt vendored
View File

@@ -984,6 +984,11 @@ go.opentelemetry.io/contrib/bridges/prometheus
# go.opentelemetry.io/contrib/exporters/autoexport v0.60.0
## explicit; go 1.22.0
go.opentelemetry.io/contrib/exporters/autoexport
# go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0
## explicit; go 1.22.0
go.opentelemetry.io/contrib/instrumentation/runtime
go.opentelemetry.io/contrib/instrumentation/runtime/internal/deprecatedruntime
go.opentelemetry.io/contrib/instrumentation/runtime/internal/x
# go.opentelemetry.io/otel v1.35.0
## explicit; go 1.22.0
go.opentelemetry.io/otel