Skip to content
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
84 changes: 49 additions & 35 deletions templates/code/go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,17 @@ that carries the registry's schema URL:

The namespace is the first segment of a name, or the second when the first is
listed in `vendor_prefixes`. Identifiers drop that prefix, so `myapp.task.id`
becomes `myappattr.TaskIDAttr`.
becomes `myappattr.TaskID`.

### Attributes

Each attribute becomes a struct that can only be built through its constructor
or, for enums, through its members:

```go
myappattr.NewTaskIDAttr("task_0001").KeyValue()
myappattr.NewTaskID("task_0001").KeyValue()
myappattr.TaskStateQueued.KeyValue()
myappattr.TaskIDAttr{}.Key()
myappattr.TaskID{}.Key()
```

Supported types are `string`, `int`, `double`, `boolean`, their array forms, and
Expand Down Expand Up @@ -139,35 +139,49 @@ The zero `Logger` emits log records that go nowhere.
### Metrics

Each metric becomes a struct wrapping the matching synchronous instrument, built
with `New<Name><Instrument>(meter)`. Required attributes are typed parameters of
with `New<Name>(meter)`. Required attributes are typed parameters of
`Add` or `Record`, and every other attribute is a trailing option:

```go
duration, err := myappmetric.NewTaskDurationHistogram(meter)
duration.Record(ctx, 1.5, myappattr.NewTaskIDAttr("task_0001"),
myappmetric.WithTaskDurationHistogramTaskState(myappattr.TaskStateRunning))
duration, err := myappmetric.NewTaskDuration(meter)
duration.Record(ctx, 1.5, myappattr.NewTaskID("task_0001"),
myappmetric.WithTaskDurationTaskState(myappattr.TaskStateRunning))
```

Counters, up-down counters, and gauges also get an asynchronous form, built
with `New<Name>Observable<Instrument>(meter, callback)`. The callback receives a
with `New<Name>Observable(meter, callback)`. The callback receives a
typed observer whose `Observe` takes the same parameters and options as the
synchronous method, so an observation cannot skip a required attribute or
attach one the convention does not declare:

```go
_, err := myappmetric.NewTaskActiveObservableUpDownCounter(meter,
func(ctx context.Context, o myappmetric.TaskActiveUpDownCounterObserver) error {
_, err := myappmetric.NewTaskActiveObservable(meter,
func(ctx context.Context, o myappmetric.TaskActiveObserver) error {
o.Observe(int64(queue.Len()), myappattr.TaskStateQueued)
return nil
})
```

The callback has its own type, `<Name><Instrument>Callback`, so it can be
The callback has its own type, `<Name>Callback`, so it can be
declared ahead of the constructor. It is registered when the instrument is
created and runs on every collection. An error it returns is returned by that
collection, and the observations made before it are still recorded. Histograms
have no asynchronous form in OpenTelemetry, so they get none.

Like the official `semconv` packages, each instrument type, synchronous or
observable, reports its registry metadata through `Name`, `Unit`, and
`Description`, which work on the zero value, and exposes the underlying
OpenTelemetry instrument through `Inst`:

```go
myappmetric.TaskDuration{}.Name() // "myapp.task.duration"
duration.Inst() // metric.Float64Histogram
```

`Unit` and `Description` return an empty string when the registry sets none.
A refinement reports the name of the metric it refines, since both record into
one instrument.

Go cannot check a requirement condition, so conditionally required,
recommended, and opt-in attributes are all options. The option's doc comment
carries the level and its condition, such as
Expand All @@ -185,7 +199,7 @@ plain Go value instead, with the key filled in, so an imported enum is taken as
the Go value of its members:

```go
failed.Add(ctx, 1, "timeout", myappattr.NewTaskIDAttr("task_0001"))
failed.Add(ctx, 1, "timeout", myappattr.NewTaskID("task_0001"))
```

Map its namespace to the Go import path of a dependency registry that was
Expand All @@ -194,7 +208,7 @@ attribute is taken as the typed value from `<path>/<ns>attr` instead, exactly
like a local attribute:

```go
failed.Add(ctx, 1, errorattr.TypeTimeout, myappattr.NewTaskIDAttr("task_0001"))
failed.Add(ctx, 1, errorattr.TypeTimeout, myappattr.NewTaskID("task_0001"))
```

The map's key is the attribute's namespace as this registry's own
Expand Down Expand Up @@ -260,8 +274,8 @@ with the attributes and requirement levels the refinement declares. A
refinement that makes an attribute required therefore makes it a parameter:

```go
retried, err := myappmetric.NewTaskRetryDurationHistogram(meter)
retried.Record(ctx, 1.5, myappattr.NewTaskIDAttr("task_0001"), myappattr.TaskStateDone)
retried, err := myappmetric.NewTaskRetryDuration(meter)
retried.Record(ctx, 1.5, myappattr.NewTaskID("task_0001"), myappattr.TaskStateDone)
```

It records under the name of the metric it refines, created with that metric's
Expand All @@ -271,27 +285,27 @@ is generated only when the metric it refines is.

### Spans

Each span becomes a `Start<Name>` function and a `<Name>Span` it returns. The
Each span becomes a `Start<Name>` function and a `<Name>` it returns. The
starter takes the `<root>tracer.Tracer`, then every required attribute as a
typed parameter, then the rest as options, and starts the span with the kind
the registry gives it:

```go
ctx, span := myappspan.StartTaskDispatch(ctx, tracer,
workerattr.NewHostNameAttr("worker-1"),
workerattr.NewHostPortAttr(8080),
myappspan.WithTaskDispatchTaskID(myappattr.NewTaskIDAttr("task_0001")),
workerattr.NewHostName("worker-1"),
workerattr.NewHostPort(8080),
myappspan.WithTaskDispatchTaskID(myappattr.NewTaskID("task_0001")),
)
defer span.End()
span.SetAttributes(myappspan.WithTaskDispatchQueueName(myappattr.NewQueueNameAttr("reports")))
span.SetAttributes(myappspan.WithTaskDispatchQueueName(myappattr.NewQueueName("reports")))
```

Every attribute given to the starter is set as the span starts, since a sampler
sees only those. An option for an attribute marked `sampling_relevant` returns
only `<Name>StartAttr`, which the starter accepts, while every other option
returns `<Name>Attr`, which `SetAttributes` also accepts after the span started.
`<Name>Span` also has `End`, `RecordError`, `SetStatus`, and `Span` for the
underlying `trace.Span`. The zero `<Name>Span` records nothing.
`<Name>` also has `End`, `RecordError`, `SetStatus`, and `Span` for the
underlying `trace.Span`. The zero `<Name>` records nothing.

The span name comes from `name.note`, read only for a name template: a single
code span holding at least one `{placeholder}`, each the key of an attribute
Expand All @@ -318,7 +332,7 @@ has nothing left after its namespace. Rather than failing, it renders
unprefixed, in its own `<name>span` package:

```go
ctx, span := heartbeatspan.Start(ctx, tracer, heartbeatattr.NewSequenceAttr(1))
ctx, span := heartbeatspan.Start(ctx, tracer, heartbeatspan.Name{}, heartbeatattr.NewSequence(1))
defer span.End()
```

Expand All @@ -331,7 +345,7 @@ severity and the timestamp:

```go
myappevent.EmitTaskFinished(ctx, logger, myappattr.TaskStateDone,
myappevent.WithTaskFinishedTaskAttempt(myappattr.NewTaskAttemptAttr(2)),
myappevent.WithTaskFinishedTaskAttempt(myappattr.NewTaskAttempt(2)),
myappevent.WithTaskFinishedSeverity(log.SeverityWarn),
)
```
Expand All @@ -351,21 +365,21 @@ event, has nothing left after its namespace. Rather than failing, it renders
unprefixed, in its own `<name>event` package:

```go
exceptionevent.Emit(ctx, logger, exceptionattr.NewTypeAttr("ValueError"),
exceptionevent.WithMessage(exceptionattr.NewMessageAttr("division by zero")),
exceptionevent.Emit(ctx, logger, exceptionattr.NewType("ValueError"),
exceptionevent.WithMessage(exceptionattr.NewMessage("division by zero")),
)
```

### Entities

Each entity becomes a `<Name>Entity` value type built by `New<Name>Entity`.
Each entity becomes a `<Name>` value type built by `New<Name>`.
A required attribute of the entity's identity or description is a typed
parameter of the constructor, and every other attribute is a trailing option:

```go
host := myappentity.NewHostEntity(
myappattr.NewHostNameAttr("worker-1"),
myappentity.WithHostEntityHostType(myappattr.HostTypeVirtual),
host := myappentity.NewHost(
myappattr.NewHostName("worker-1"),
myappentity.WithHostHostType(myappattr.HostTypeVirtual),
)
```

Expand All @@ -377,7 +391,7 @@ schema. Resources from the same registry merge cleanly:
res, err := resource.Merge(host.Resource(), queue.Resource())
```

The zero `<Name>Entity` carries no attributes. Identity attributes come
The zero `<Name>` carries no attributes. Identity attributes come
before description attributes in both the constructor and the doc comment, so
a reader can tell at a glance which attributes identify the entity and which
merely describe it.
Expand All @@ -393,7 +407,7 @@ after its namespace. Rather than failing, it renders
unprefixed, in its own `<name>entity` package:

```go
host := hostentity.NewEntity(hostattr.NewIDAttr("h-1"))
host := hostentity.NewEntity(hostattr.NewID("h-1"))
```

### Deprecation
Expand Down Expand Up @@ -480,9 +494,9 @@ that same `Resource`. Pass its attributes to `resource.New` under the
registry's schema URL:

```go
host := myappentity.NewHostEntity(
myappattr.NewHostNameAttr(hostname),
myappentity.WithHostEntityHostType(myappattr.HostTypeVirtual),
host := myappentity.NewHost(
myappattr.NewHostName(hostname),
myappentity.WithHostHostType(myappattr.HostTypeVirtual),
)

res, err := resource.New(ctx,
Expand Down
2 changes: 1 addition & 1 deletion templates/code/go/attributes.go.j2
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ package {{ ctx.namespace }}attr
import "go.opentelemetry.io/otel/attribute"
{% for attr in ctx.attributes -%}
{%- set base = n.exported(attr.short_name) -%}
{%- set name = base ~ "Attr" -%}
{%- set name = base -%}
{%- set key = n.unexported(attr.short_name) ~ "Key" -%}
{%- set is_enum = attr.type is mapping -%}
{%- set deprecated = d.notice(attr) %}
Expand Down
4 changes: 2 additions & 2 deletions templates/code/go/entities.go.j2
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{%- if a.imported -%}
{{ a.attr_type | map_text("attribute_field_type") }}
{%- else -%}
{{ a.attr_namespace }}attr.{{ n.exported(a.attr_short_name) }}Attr
{{ a.attr_namespace }}attr.{{ n.exported(a.attr_short_name) }}
{%- endif -%}
{%- endmacro -%}
{%- macro attr_kv(a, v) -%}
Expand Down Expand Up @@ -34,7 +34,7 @@ import (
{% for def in ctx.entities %}
{%- set name = n.exported(def.short_name) %}
{%- set private = n.unexported(def.short_name) if def.short_name else "bare" %}
{%- set entity = name ~ "Entity" %}
{%- set entity = name or "Entity" %}
{%- set deprecated = d.notice(def) %}
{%- set opt_in = "Opt-in: the convention records `" ~ def.id ~ "` only when a user asks for it." if def.requirement_level == "opt_in" else none -%}
{% if def.optional %}
Expand Down
2 changes: 1 addition & 1 deletion templates/code/go/events.go.j2
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{%- if a.imported -%}
{{ a.attr_type | map_text("attribute_field_type") }}
{%- else -%}
{{ a.attr_namespace }}attr.{{ n.exported(a.attr_short_name) }}Attr
{{ a.attr_namespace }}attr.{{ n.exported(a.attr_short_name) }}
{%- endif -%}
{%- endmacro -%}
{%- macro attr_kv(a, v) -%}
Expand Down
68 changes: 55 additions & 13 deletions templates/code/go/metrics.go.j2
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{%- if a.imported -%}
{{ a.attr_type | map_text("attribute_field_type") }}
{%- else -%}
{{ a.attr_namespace }}attr.{{ n.exported(a.attr_short_name) }}Attr
{{ a.attr_namespace }}attr.{{ n.exported(a.attr_short_name) }}
{%- endif -%}
{%- endmacro -%}
{%- macro attr_kv(a, v) -%}
Expand Down Expand Up @@ -33,8 +33,8 @@ import (
)
{% for def in ctx.metrics %}
{%- set suffix = def.instrument | map_text("metric_instrument") %}
{%- set name = n.exported(def.short_name) ~ suffix %}
{%- set private = n.unexported(def.short_name) ~ suffix %}
{%- set name = n.exported(def.short_name) %}
{%- set private = n.unexported(def.short_name) %}
{%- set method = def.instrument | map_text("metric_instrument_method") %}
{%- set arg = def.instrument | map_text("metric_instrument_arg") %}
{%- set value_type = def.annotations.code_generation.metric_value_type | map_text("metric_value_type") %}
Expand Down Expand Up @@ -66,20 +66,41 @@ type {{ name }} struct{ inst metric.{{ instrument }} }

{{ ["New" ~ name ~ " creates the `" ~ def.instrument_name ~ "` instrument from meter.", opt_in, deprecated] | select | join("\n\n") | comment }}
func New{{ name }}(meter {{ n.root() }}meter.Meter) ({{ name }}, error) {
var m {{ name }}
inst, err := meter.MetricMeter().{{ instrument }}(
"{{ def.instrument_name }}",
metric.WithDescription({{ def.description | trim | tojson }}),
metric.WithUnit({{ def.unit | tojson }}),
m.Name(),
metric.WithDescription(m.Description()),
metric.WithUnit(m.Unit()),
{%- if def.bucket_boundaries %}
metric.WithExplicitBucketBoundaries({{ def.bucket_boundaries | join(", ") }}),
{%- endif %}
)
if err != nil {
return {{ name }}{}, fmt.Errorf("create the %s instrument: %w", "{{ def.instrument_name }}", err)
return {{ name }}{}, fmt.Errorf("create the %s instrument: %w", m.Name(), err)
}
return {{ name }}{inst: inst}, nil
}

{{ ["Name returns the name `" ~ def.name ~ "` records under."] | join | comment }}
func ({{ name }}) Name() string {
return {{ def.instrument_name | tojson }}
}

{{ ["Unit returns the unit of `" ~ def.name ~ "`."] | join | comment }}
func ({{ name }}) Unit() string {
return {{ (def.unit or "") | tojson }}
}

{{ ["Description returns the description of `" ~ def.name ~ "`."] | join | comment }}
func ({{ name }}) Description() string {
return {{ (def.description or "") | trim | tojson }}
}

{{ ["Inst returns the underlying metric." ~ instrument ~ ", or nil for the zero " ~ name ~ "."] | join | comment }}
func (m {{ name }}) Inst() metric.{{ instrument }} {
return m.inst
}

{{ [method ~ " records one `" ~ def.name ~ "` measurement. The zero " ~ name ~ " records nothing."] | join | comment }}
func (m {{ name }}) {{ method }}(ctx context.Context, {{ arg }} {{ gotype }}{% for req in def.required %}, {{ n.unexported(req.attr_param) }} {{ attr_type(req) }}{% endfor %}{% if def.optional %}, opts ...{{ name }}Attr{% endif %}) {
if m.inst == nil {
Expand All @@ -101,7 +122,7 @@ func (m {{ name }}) {{ method }}(ctx context.Context, {{ arg }} {{ gotype }}{% f
{%- endif %}
}
{%- if def.instrument != "histogram" %}
{%- set observable = n.exported(def.short_name) ~ "Observable" ~ suffix %}
{%- set observable = name ~ "Observable" %}
{%- set observer = name ~ "Observer" %}
{%- set callback = name ~ "Callback" %}

Expand Down Expand Up @@ -141,21 +162,42 @@ type {{ observable }} struct {

{{ ["New" ~ observable ~ " creates the `" ~ def.instrument_name ~ "` instrument from meter and registers callback to observe it on every collection.", opt_in, deprecated] | select | join("\n\n") | comment }}
func New{{ observable }}(meter {{ n.root() }}meter.Meter, callback {{ callback }}) ({{ observable }}, error) {
var m {{ observable }}
if callback == nil {
return {{ observable }}{}, fmt.Errorf("create the %s instrument: the callback is nil", "{{ def.instrument_name }}")
return {{ observable }}{}, fmt.Errorf("create the %s instrument: the callback is nil", m.Name())
}
inst, err := meter.MetricMeter().{{ value_type }}Observable{{ suffix }}(
"{{ def.instrument_name }}",
metric.WithDescription({{ def.description | trim | tojson }}),
metric.WithUnit({{ def.unit | tojson }}),
m.Name(),
metric.WithDescription(m.Description()),
metric.WithUnit(m.Unit()),
metric.With{{ value_type }}Callback(func(ctx context.Context, o metric.{{ value_type }}Observer) error {
return callback(ctx, {{ observer }}{o: o})
}),
)
if err != nil {
return {{ observable }}{}, fmt.Errorf("create the %s instrument: %w", "{{ def.instrument_name }}", err)
return {{ observable }}{}, fmt.Errorf("create the %s instrument: %w", m.Name(), err)
}
return {{ observable }}{inst: inst}, nil
}

{{ ["Name returns the name `" ~ def.name ~ "` records under."] | join | comment }}
func ({{ observable }}) Name() string {
return {{ name }}{}.Name()
}

{{ ["Unit returns the unit of `" ~ def.name ~ "`."] | join | comment }}
func ({{ observable }}) Unit() string {
return {{ name }}{}.Unit()
}

{{ ["Description returns the description of `" ~ def.name ~ "`."] | join | comment }}
func ({{ observable }}) Description() string {
return {{ name }}{}.Description()
}

{{ ["Inst returns the underlying metric." ~ value_type ~ "Observable" ~ suffix ~ ", or nil for the zero " ~ observable ~ "."] | join | comment }}
func (m {{ observable }}) Inst() metric.{{ value_type }}Observable{{ suffix }} {
return m.inst
}
{%- endif %}
{% endfor %}{% endfilter %}
4 changes: 2 additions & 2 deletions templates/code/go/spans.go.j2
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
{%- if a.imported -%}
{{ a.attr_type | map_text("attribute_field_type") }}
{%- else -%}
{{ a.attr_namespace }}attr.{{ n.exported(a.attr_short_name) }}Attr
{{ a.attr_namespace }}attr.{{ n.exported(a.attr_short_name) }}
{%- endif -%}
{%- endmacro -%}
{%- macro attr_kv(a, v) -%}
Expand Down Expand Up @@ -35,7 +35,7 @@ import (
{% for def in ctx.spans %}
{%- set name = n.exported(def.short_name) %}
{%- set private = n.unexported(def.short_name) if def.short_name else "bare" %}
{%- set span = name ~ "Span" %}
{%- set span = name or "Span" %}
{%- set start = "Start" ~ name %}
{%- set deprecated = d.notice(def) %}
{%- set opt_in = "Opt-in: the convention records `" ~ def.id ~ "` only when a user asks for it." if def.requirement_level == "opt_in" else none -%}
Expand Down
Loading
Loading