Skip to content

Commit 6c171ff

Browse files
authored
Merge branch 'main' into main
2 parents 97b04e7 + 911ed35 commit 6c171ff

29 files changed

+1518
-523
lines changed

.github/workflows/benchmark.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
permissions:
2929
contents: write # required for pushing to gh-pages branch
3030
name: Benchmarks
31-
runs-on: oracle-bare-metal-64cpu-512gb-x86-64
31+
runs-on: oracle-bare-metal-64cpu-1024gb-x86-64-ubuntu-24
3232
strategy:
3333
matrix:
3434
shard: ${{ fromJson(needs.sharding-benchmark.outputs.shards) }}

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,17 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1212

1313
- Add `IsRandom` and `WithRandom` on `TraceFlags`, and `IsRandom` on `SpanContext` in `go.opentelemetry.io/otel/trace` for [W3C Trace Context Level 2 Random Trace ID Flag](https://www.w3.org/TR/trace-context-2/#random-trace-id-flag) support. (#8012)
1414
- Add service detection with `WithService` in `go.opentelemetry.io/otel/sdk/resource`. (#7642)
15+
- Add `DefaultWithContext` and `EnvironmentWithContext` in `go.opentelemetry.io/otel/sdk/resource` to support plumbing `context.Context` through default and environment detectors. (#8051)
1516
- Support attributes with empty value (`attribute.EMPTY`) in OTLP exporters (`go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`, `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc`, `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`, `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`, `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`, `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`). (#8038)
1617
- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest`. (#8038)
18+
- Add support for per-series start time tracking for cumulative metrics in `go.opentelemetry.io/otel/sdk/metric`.
19+
Set `OTEL_GO_X_PER_SERIES_START_TIMESTAMPS=true` to enable. (#8060)
1720

1821
### Changed
1922

2023
- Introduce the `EMPTY` Type in `go.opentelemetry.io/otel/attribute` to reflect that an empty value is now a valid value, with `INVALID` remaining as a deprecated alias of `EMPTY`. (#8038)
2124
- Refactor slice handling in `go.opentelemetry.io/otel/attribute` to optimize short slice values with fixed-size fast paths. (#8039)
25+
- Improve performance of span metric recording in `go.opentelemetry.io/otel/sdk/trace` by returning early if self-observability is not enabled. (#8067)
2226

2327
### Deprecated
2428

exporters/stdout/stdoutlog/go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ require (
99
github.com/stretchr/testify v1.11.1
1010
go.opentelemetry.io/otel v1.42.0
1111
go.opentelemetry.io/otel/log v0.18.0
12+
go.opentelemetry.io/otel/metric v1.42.0
1213
go.opentelemetry.io/otel/sdk v1.42.0
1314
go.opentelemetry.io/otel/sdk/log v0.18.0
1415
go.opentelemetry.io/otel/sdk/log/logtest v0.18.0
16+
go.opentelemetry.io/otel/sdk/metric v1.42.0
1517
go.opentelemetry.io/otel/trace v1.42.0
1618
)
1719

@@ -23,7 +25,6 @@ require (
2325
github.com/google/uuid v1.6.0 // indirect
2426
github.com/pmezard/go-difflib v1.0.0 // indirect
2527
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
26-
go.opentelemetry.io/otel/metric v1.42.0 // indirect
2728
golang.org/x/sys v0.42.0 // indirect
2829
gopkg.in/yaml.v3 v3.0.1 // indirect
2930
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// Package internal provides internal functionality for the stdoutlog
5+
// package.
6+
package internal // import "go.opentelemetry.io/otel/exporters/stdout/stdoutlog/internal"
7+
8+
//go:generate gotmpl --body=../../../../internal/shared/x/x.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/otel/exporters/stdout/stdoutlog\" }" --out=x/x.go
9+
//go:generate gotmpl --body=../../../../internal/shared/x/x_test.go.tmpl "--data={}" --out=x/x_test.go
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// Package observ provides observability metrics for OTLP log exporters.
5+
// This is an experimental feature controlled by the x.Observability feature flag.
6+
package observ // import "go.opentelemetry.io/otel/exporters/stdout/stdoutlog/internal/observ"
7+
8+
import (
9+
"context"
10+
"errors"
11+
"fmt"
12+
"sync"
13+
"time"
14+
15+
"go.opentelemetry.io/otel/exporters/stdout/stdoutlog/internal/x"
16+
17+
"go.opentelemetry.io/otel"
18+
"go.opentelemetry.io/otel/attribute"
19+
"go.opentelemetry.io/otel/exporters/stdout/stdoutlog/internal"
20+
"go.opentelemetry.io/otel/metric"
21+
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
22+
"go.opentelemetry.io/otel/semconv/v1.40.0/otelconv"
23+
)
24+
25+
const (
26+
// ScopeName is the unique name of the meter used for instrumentation.
27+
ScopeName = "go.opentelemetry.io/otel/exporters/stdoutlog/internal/observ"
28+
29+
// ComponentType uniquely identifies the OpenTelemetry Exporter component
30+
// being instrumented.
31+
//
32+
// The STDOUT log exporter is not a standardized OTel component type, so
33+
// it uses the Go package prefixed type name to ensure uniqueness and
34+
// identity.
35+
ComponentType = "go.opentelemetry.io/otel/exporters/stdout/stdoutlog.Exporter"
36+
37+
// Version is the current version of this instrumentation.
38+
//
39+
// This matches the version of the exporter.
40+
Version = internal.Version
41+
)
42+
43+
var (
44+
addOptPool = &sync.Pool{
45+
New: func() any {
46+
const n = 1
47+
s := make([]metric.AddOption, 0, n)
48+
return &s
49+
},
50+
}
51+
attrsPool = &sync.Pool{
52+
New: func() any {
53+
const n = 1 + // component.name
54+
1 + // component.type
55+
1 // error.type
56+
s := make([]attribute.KeyValue, 0, n)
57+
return &s
58+
},
59+
}
60+
recordOptPool = &sync.Pool{
61+
New: func() any {
62+
const n = 1
63+
s := make([]metric.RecordOption, 0, n)
64+
return &s
65+
},
66+
}
67+
)
68+
69+
func get[T any](pool *sync.Pool) *[]T {
70+
return pool.Get().(*[]T)
71+
}
72+
73+
func put[T any](pool *sync.Pool, value *[]T) {
74+
*value = (*value)[:0]
75+
pool.Put(value)
76+
}
77+
78+
// Instrumentation is experimental instrumentation for the exporter.
79+
type Instrumentation struct {
80+
inflight metric.Int64UpDownCounter
81+
exported metric.Int64Counter
82+
duration metric.Float64Histogram
83+
84+
attrs []attribute.KeyValue
85+
addOpt metric.AddOption
86+
recOpt metric.RecordOption
87+
}
88+
89+
// GetComponentName returns the constant name for the exporter with the
90+
// provided id.
91+
func GetComponentName(id int64) string {
92+
return fmt.Sprintf("%s/%d", ComponentType, id)
93+
}
94+
95+
func getAttrs(id int64) []attribute.KeyValue {
96+
attrs := make([]attribute.KeyValue, 0, 2)
97+
attrs = append(attrs,
98+
semconv.OTelComponentName(GetComponentName(id)),
99+
semconv.OTelComponentNameKey.String(ComponentType))
100+
101+
return attrs
102+
}
103+
104+
// NewInstrumentation returns instrumentation for stdlog exporter.
105+
func NewInstrumentation(id int64) (*Instrumentation, error) {
106+
if !x.Observability.Enabled() {
107+
return nil, nil
108+
}
109+
110+
inst := &Instrumentation{}
111+
112+
mp := otel.GetMeterProvider()
113+
m := mp.Meter(
114+
ScopeName,
115+
metric.WithInstrumentationVersion(Version),
116+
metric.WithSchemaURL(semconv.SchemaURL),
117+
)
118+
119+
var err error
120+
121+
inflight, e := otelconv.NewSDKExporterLogInflight(m)
122+
if e != nil {
123+
e = fmt.Errorf("failed to create the inflight metric: %w", e)
124+
err = errors.Join(err, e)
125+
}
126+
inst.inflight = inflight.Inst()
127+
128+
exported, e := otelconv.NewSDKExporterLogExported(m)
129+
if e != nil {
130+
e = fmt.Errorf("failed to create the exported metric: %w", e)
131+
err = errors.Join(err, e)
132+
}
133+
inst.exported = exported.Inst()
134+
135+
duration, e := otelconv.NewSDKExporterOperationDuration(m)
136+
if e != nil {
137+
e = fmt.Errorf("failed to create the duration metric: %w", e)
138+
err = errors.Join(err, e)
139+
}
140+
inst.duration = duration.Inst()
141+
142+
if err != nil {
143+
return nil, err
144+
}
145+
inst.attrs = getAttrs(id)
146+
inst.addOpt = metric.WithAttributeSet(attribute.NewSet(inst.attrs...))
147+
inst.recOpt = metric.WithAttributeSet(attribute.NewSet(inst.attrs...))
148+
return inst, nil
149+
}
150+
151+
// ExportLogs instruments the ExportLogs method of the exporter. It returns
152+
// an [ExportOp] that must have its [ExportOp.End] method called when the
153+
// ExportLogs method returns.
154+
func (i *Instrumentation) ExportLogs(ctx context.Context, count int64) ExportOp {
155+
start := time.Now()
156+
157+
addOpt := get[metric.AddOption](addOptPool)
158+
defer put(addOptPool, addOpt)
159+
*addOpt = append(*addOpt, i.addOpt)
160+
161+
i.inflight.Add(ctx, count, *addOpt...)
162+
163+
return ExportOp{
164+
count: count,
165+
ctx: ctx,
166+
inst: i,
167+
start: start,
168+
}
169+
}
170+
171+
// ExportOp tracks the operation being observed by [Instrumentation.ExportLogs].
172+
type ExportOp struct {
173+
count int64
174+
ctx context.Context
175+
inst *Instrumentation
176+
start time.Time
177+
}
178+
179+
// End completes the observation of the operation being observed by a call to
180+
// [Instrumentation.ExportLogs].
181+
// Any error that is encountered is provided as err.
182+
//
183+
// If err is not nil, all logs will be recorded as failures unless error is of
184+
// type [internal.PartialSuccess]. In the case of a PartialSuccess, the number
185+
// of successfully exported logs will be determined by inspecting the
186+
// RejectedItems field of the PartialSuccess.
187+
func (e ExportOp) End(err error) {
188+
addOpt := get[metric.AddOption](addOptPool)
189+
defer put(addOptPool, addOpt)
190+
*addOpt = append(*addOpt, e.inst.addOpt)
191+
192+
e.inst.inflight.Add(e.ctx, -e.count, *addOpt...)
193+
194+
success := successful(err, e.count)
195+
e.inst.exported.Add(e.ctx, success, *addOpt...)
196+
197+
if err != nil {
198+
// Add the error.type attribute to the attribute set.
199+
attrs := get[attribute.KeyValue](attrsPool)
200+
defer put(attrsPool, attrs)
201+
*attrs = append(*attrs, e.inst.attrs...)
202+
*attrs = append(*attrs, semconv.ErrorType(err))
203+
204+
o := metric.WithAttributeSet(attribute.NewSet(*attrs...))
205+
206+
*addOpt = append((*addOpt)[:0], o)
207+
e.inst.exported.Add(e.ctx, e.count-success, *addOpt...)
208+
}
209+
210+
recordOpt := get[metric.RecordOption](recordOptPool)
211+
defer put(recordOptPool, recordOpt)
212+
213+
*recordOpt = append(*recordOpt, e.inst.recordOption(err))
214+
e.inst.duration.Record(e.ctx, time.Since(e.start).Seconds(), *recordOpt...)
215+
}
216+
217+
func (i *Instrumentation) recordOption(err error) metric.RecordOption {
218+
if err == nil {
219+
return i.recOpt
220+
}
221+
attrs := get[attribute.KeyValue](attrsPool)
222+
defer put(attrsPool, attrs)
223+
224+
*attrs = append(*attrs, i.attrs...)
225+
*attrs = append(*attrs, semconv.ErrorType(err))
226+
return metric.WithAttributeSet(attribute.NewSet(*attrs...))
227+
}
228+
229+
// successful returns the number of successfully exported logs out of the n
230+
// that were exported based on the provided error.
231+
//
232+
// If err is nil, n is returned. All logs were successfully exported.
233+
//
234+
// If err is not nil and not an [internal.PartialSuccess] error, 0 is returned.
235+
// It is assumed all logs failed to be exported.
236+
//
237+
// If err is an [internal.PartialSuccess] error, the number of successfully
238+
// exported logs is computed by subtracting the RejectedItems field from n. If
239+
// RejectedItems is negative, n is returned. If RejectedItems is greater than
240+
// n, 0 is returned.
241+
func successful(err error, n int64) int64 {
242+
if err == nil {
243+
return n // All logs successfully exported.
244+
}
245+
// Split rejected calculation so successful is inlineable.
246+
return n - rejectedCount(n, err)
247+
}
248+
249+
var errPool = sync.Pool{
250+
New: func() any {
251+
return new(internal.PartialSuccess)
252+
},
253+
}
254+
255+
// rejectedCount returns how many out of the n logs exported were rejected based on
256+
// the provided non-nil err.
257+
func rejectedCount(n int64, err error) int64 {
258+
ps := errPool.Get().(*internal.PartialSuccess)
259+
defer errPool.Put(ps)
260+
261+
// check for partial success
262+
if errors.As(err, ps) {
263+
return min(max(ps.RejectedItems, 0), n)
264+
}
265+
// all logs exported
266+
return n
267+
}

0 commit comments

Comments
 (0)