From 435a386c0551fa7a9fee78e69887015a1f11ffe6 Mon Sep 17 00:00:00 2001 From: Tigran Najaryan Date: Thu, 7 Jul 2022 15:41:43 -0400 Subject: [PATCH] Introduce "split" metric schema transformation This is a new transformation type that allows to describe a change where a metric is converted to several other metrics by eliminating an attribute. An example of such change that happened recently is this: https://github.com/open-telemetry/opentelemetry-specification/pull/2617 This PR implements specification change https://github.com/open-telemetry/opentelemetry-specification/pull/2653 This PR creates package v1.1 for the new functionality. The old package v1.0 remains unchanged. --- CHANGELOG.md | 1 + schema/README.md | 4 +- schema/internal/parser_checks.go | 74 +++++++ schema/internal/parser_checks_test.go | 41 ++++ schema/v1.0/parser.go | 56 +---- schema/v1.0/parser_test.go | 199 +++++++++--------- .../testdata/unsupported-file-format.yaml | 1 + schema/v1.1/ast/ast_schema.go | 50 +++++ schema/v1.1/ast/metrics.go | 52 +++++ schema/v1.1/parser.go | 62 ++++++ schema/v1.1/parser_test.go | 174 +++++++++++++++ .../testdata/unsupported-file-format.yaml | 10 + schema/v1.1/testdata/valid-example.yaml | 144 +++++++++++++ schema/v1.1/types/types.go | 26 +++ 14 files changed, 741 insertions(+), 153 deletions(-) create mode 100644 schema/internal/parser_checks.go create mode 100644 schema/internal/parser_checks_test.go create mode 100644 schema/v1.1/ast/ast_schema.go create mode 100644 schema/v1.1/ast/metrics.go create mode 100644 schema/v1.1/parser.go create mode 100644 schema/v1.1/parser_test.go create mode 100644 schema/v1.1/testdata/unsupported-file-format.yaml create mode 100644 schema/v1.1/testdata/valid-example.yaml create mode 100644 schema/v1.1/types/types.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bff4ee4bf7..c7ed2b320db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add support for `opentracing.TextMap` format in the `Inject` and `Extract` methods of the `"go.opentelemetry.io/otel/bridge/opentracing".BridgeTracer` type. (#2911) +- Add support for Schema Files format 1.1.x (metric "split" transform). (#2999) ### Changed diff --git a/schema/README.md b/schema/README.md index 0b02a5f1336..2301b439fac 100644 --- a/schema/README.md +++ b/schema/README.md @@ -11,9 +11,9 @@ then import the corresponding package and use the `Parse` or `ParseFile` functio like this: ```go -import schema "go.opentelemetry.io/otel/schema/v1.0" +import schema "go.opentelemetry.io/otel/schema/v1.1" -// Load the schema from a file in v1.0.x file format. +// Load the schema from a file in v1.1.x file format. func loadSchemaFromFile() error { telSchema, err := schema.ParseFile("schema-file.yaml") if err != nil { diff --git a/schema/internal/parser_checks.go b/schema/internal/parser_checks.go new file mode 100644 index 00000000000..1ea46939444 --- /dev/null +++ b/schema/internal/parser_checks.go @@ -0,0 +1,74 @@ +// Copyright The OpenTelemetry Authors +// +// 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. + +package internal // import "go.opentelemetry.io/otel/schema/internal" + +import ( + "errors" + "fmt" + "net/url" + "strconv" + "strings" + + "github.com/Masterminds/semver/v3" +) + +// CheckFileFormatField validates the file format field according to the rules here: +// https://github.com/open-telemetry/oteps/blob/main/text/0152-telemetry-schemas.md#schema-file-format-number +func CheckFileFormatField(fileFormat string, supportedFormatMajor, supportedFormatMinor int) error { + // Verify that the version number in the file is a semver. + fileFormatParsed, err := semver.StrictNewVersion(fileFormat) + if err != nil { + return fmt.Errorf( + "invalid schema file format version number %q (expected semver): %w", + fileFormat, err, + ) + } + + // Check that the major version number in the file is the same as what we expect. + if fileFormatParsed.Major() != uint64(supportedFormatMajor) { + return fmt.Errorf( + "this library cannot parse file formats with major version other than %v", + supportedFormatMajor, + ) + } + + // Check that the file minor version number is not greater than + // what is requested supports. + if fileFormatParsed.Minor() > uint64(supportedFormatMinor) { + supportedFormatMajorMinor := strconv.Itoa(supportedFormatMajor) + "." + + strconv.Itoa(supportedFormatMinor) // 1.0 + + return fmt.Errorf( + "unsupported schema file format minor version number, expected no newer than %v, got %v", + supportedFormatMajorMinor+".x", fileFormat, + ) + } + + // Patch, prerelease and metadata version number does not matter, so we don't check it. + + return nil +} + +// CheckSchemaURL verifies that schemaURL is valid. +func CheckSchemaURL(schemaURL string) error { + if strings.TrimSpace(schemaURL) == "" { + return errors.New("schema_url field is missing") + } + + if _, err := url.Parse(schemaURL); err != nil { + return fmt.Errorf("invalid URL specified in schema_url field: %w", err) + } + return nil +} diff --git a/schema/internal/parser_checks_test.go b/schema/internal/parser_checks_test.go new file mode 100644 index 00000000000..dab407de51c --- /dev/null +++ b/schema/internal/parser_checks_test.go @@ -0,0 +1,41 @@ +// Copyright The OpenTelemetry Authors +// +// 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. + +package internal // import "go.opentelemetry.io/otel/schema/internal" + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCheckFileFormatField(t *testing.T) { + // Invalid file format version numbers. + assert.Error(t, CheckFileFormatField("not a semver", 1, 0)) + assert.Error(t, CheckFileFormatField("2.0.0", 1, 0)) + assert.Error(t, CheckFileFormatField("1.1.0", 1, 0)) + + assert.Error(t, CheckFileFormatField("1.2.0", 1, 1)) + + // Valid cases. + assert.NoError(t, CheckFileFormatField("1.0.0", 1, 0)) + assert.NoError(t, CheckFileFormatField("1.0.1", 1, 0)) + assert.NoError(t, CheckFileFormatField("1.0.10000-alpha+4857", 1, 0)) + + assert.NoError(t, CheckFileFormatField("1.0.0", 1, 1)) + assert.NoError(t, CheckFileFormatField("1.0.1", 1, 1)) + assert.NoError(t, CheckFileFormatField("1.0.10000-alpha+4857", 1, 1)) + assert.NoError(t, CheckFileFormatField("1.1.0", 1, 1)) + assert.NoError(t, CheckFileFormatField("1.1.1", 1, 1)) +} diff --git a/schema/v1.0/parser.go b/schema/v1.0/parser.go index 413cb64c219..a284606bd9c 100644 --- a/schema/v1.0/parser.go +++ b/schema/v1.0/parser.go @@ -15,16 +15,12 @@ package schema // import "go.opentelemetry.io/otel/schema/v1.0" import ( - "fmt" "io" - "net/url" "os" - "strconv" - "strings" - "github.com/Masterminds/semver/v3" "gopkg.in/yaml.v2" + "go.opentelemetry.io/otel/schema/internal" "go.opentelemetry.io/otel/schema/v1.0/ast" ) @@ -34,10 +30,6 @@ const supportedFormatMajor = 1 // Maximum minor version number that this library supports. const supportedFormatMinor = 0 -// Maximum major+minor version number that this library supports, as a string. -var supportedFormatMajorMinor = strconv.Itoa(supportedFormatMajor) + "." + - strconv.Itoa(supportedFormatMinor) // 1.0 - // ParseFile a schema file. schemaFilePath is the file path. func ParseFile(schemaFilePath string) (*ast.Schema, error) { file, err := os.Open(schemaFilePath) @@ -56,51 +48,15 @@ func Parse(schemaFileContent io.Reader) (*ast.Schema, error) { return nil, err } - if err := checkFileFormatField(ts.FileFormat); err != nil { + err = internal.CheckFileFormatField(ts.FileFormat, supportedFormatMajor, supportedFormatMinor) + if err != nil { return nil, err } - if strings.TrimSpace(ts.SchemaURL) == "" { - return nil, fmt.Errorf("schema_url field is missing") - } - - if _, err := url.Parse(ts.SchemaURL); err != nil { - return nil, fmt.Errorf("invalid URL specified in schema_url field: %w", err) - } - - return &ts, nil -} - -// checkFileFormatField validates the file format field according to the rules here: -// https://github.com/open-telemetry/oteps/blob/main/text/0152-telemetry-schemas.md#schema-file-format-number -func checkFileFormatField(fileFormat string) error { - // Verify that the version number in the file is a semver. - fileFormatParsed, err := semver.StrictNewVersion(fileFormat) + err = internal.CheckSchemaURL(ts.SchemaURL) if err != nil { - return fmt.Errorf( - "invalid schema file format version number %q (expected semver): %w", - fileFormat, err, - ) - } - - // Check that the major version number in the file is the same as what we expect. - if fileFormatParsed.Major() != supportedFormatMajor { - return fmt.Errorf( - "this library cannot parse file formats with major version other than %v", - supportedFormatMajor, - ) - } - - // Check that the file minor version number is not greater than - // what is requested supports. - if fileFormatParsed.Minor() > supportedFormatMinor { - return fmt.Errorf( - "unsupported schema file format minor version number, expected no newer than %v, got %v", - supportedFormatMajorMinor+".x", fileFormat, - ) + return nil, err } - // Patch, prerelease and metadata version number does not matter, so we don't check it. - - return nil + return &ts, nil } diff --git a/schema/v1.0/parser_test.go b/schema/v1.0/parser_test.go index eefe3257992..72d20bd1162 100644 --- a/schema/v1.0/parser_test.go +++ b/schema/v1.0/parser_test.go @@ -28,123 +28,132 @@ func TestParseSchemaFile(t *testing.T) { ts, err := ParseFile("testdata/valid-example.yaml") assert.NoError(t, err) assert.NotNil(t, ts) - assert.EqualValues(t, &ast.Schema{ - FileFormat: "1.0.0", - SchemaURL: "https://opentelemetry.io/schemas/1.1.0", - Versions: map[types.TelemetryVersion]ast.VersionDef{ - "1.0.0": {}, - - "1.1.0": { - All: ast.Attributes{ - Changes: []ast.AttributeChange{ - {RenameAttributes: &ast.AttributeMap{ - "k8s.cluster.name": "kubernetes.cluster.name", - "k8s.namespace.name": "kubernetes.namespace.name", - "k8s.node.name": "kubernetes.node.name", - "k8s.node.uid": "kubernetes.node.uid", - "k8s.pod.name": "kubernetes.pod.name", - "k8s.pod.uid": "kubernetes.pod.uid", - "k8s.container.name": "kubernetes.container.name", - "k8s.replicaset.name": "kubernetes.replicaset.name", - "k8s.replicaset.uid": "kubernetes.replicaset.uid", - "k8s.cronjob.name": "kubernetes.cronjob.name", - "k8s.cronjob.uid": "kubernetes.cronjob.uid", - "k8s.job.name": "kubernetes.job.name", - "k8s.job.uid": "kubernetes.job.uid", - "k8s.statefulset.name": "kubernetes.statefulset.name", - "k8s.statefulset.uid": "kubernetes.statefulset.uid", - "k8s.daemonset.name": "kubernetes.daemonset.name", - "k8s.daemonset.uid": "kubernetes.daemonset.uid", - "k8s.deployment.name": "kubernetes.deployment.name", - "k8s.deployment.uid": "kubernetes.deployment.uid", - "service.namespace": "service.namespace.name", - }}, + assert.EqualValues( + t, &ast.Schema{ + FileFormat: "1.0.0", + SchemaURL: "https://opentelemetry.io/schemas/1.1.0", + Versions: map[types.TelemetryVersion]ast.VersionDef{ + "1.0.0": {}, + + "1.1.0": { + All: ast.Attributes{ + Changes: []ast.AttributeChange{ + { + RenameAttributes: &ast.AttributeMap{ + "k8s.cluster.name": "kubernetes.cluster.name", + "k8s.namespace.name": "kubernetes.namespace.name", + "k8s.node.name": "kubernetes.node.name", + "k8s.node.uid": "kubernetes.node.uid", + "k8s.pod.name": "kubernetes.pod.name", + "k8s.pod.uid": "kubernetes.pod.uid", + "k8s.container.name": "kubernetes.container.name", + "k8s.replicaset.name": "kubernetes.replicaset.name", + "k8s.replicaset.uid": "kubernetes.replicaset.uid", + "k8s.cronjob.name": "kubernetes.cronjob.name", + "k8s.cronjob.uid": "kubernetes.cronjob.uid", + "k8s.job.name": "kubernetes.job.name", + "k8s.job.uid": "kubernetes.job.uid", + "k8s.statefulset.name": "kubernetes.statefulset.name", + "k8s.statefulset.uid": "kubernetes.statefulset.uid", + "k8s.daemonset.name": "kubernetes.daemonset.name", + "k8s.daemonset.uid": "kubernetes.daemonset.uid", + "k8s.deployment.name": "kubernetes.deployment.name", + "k8s.deployment.uid": "kubernetes.deployment.uid", + "service.namespace": "service.namespace.name", + }, + }, + }, }, - }, - Resources: ast.Attributes{ - Changes: []ast.AttributeChange{ - { - RenameAttributes: &ast.AttributeMap{ - "telemetry.auto.version": "telemetry.auto_instr.version", + Resources: ast.Attributes{ + Changes: []ast.AttributeChange{ + { + RenameAttributes: &ast.AttributeMap{ + "telemetry.auto.version": "telemetry.auto_instr.version", + }, }, }, }, - }, - Spans: ast.Spans{ - Changes: []ast.SpansChange{ - { - RenameAttributes: &ast.AttributeMapForSpans{ - AttributeMap: ast.AttributeMap{ - "peer.service": "peer.service.name", + Spans: ast.Spans{ + Changes: []ast.SpansChange{ + { + RenameAttributes: &ast.AttributeMapForSpans{ + AttributeMap: ast.AttributeMap{ + "peer.service": "peer.service.name", + }, + ApplyToSpans: []types.SpanName{"HTTP GET"}, }, - ApplyToSpans: []types.SpanName{"HTTP GET"}, }, }, }, - }, - SpanEvents: ast.SpanEvents{ - Changes: []ast.SpanEventsChange{ - { - RenameEvents: &ast.RenameSpanEvents{ - EventNameMap: map[string]string{ - "exception.stacktrace": "exception.stack_trace", + SpanEvents: ast.SpanEvents{ + Changes: []ast.SpanEventsChange{ + { + RenameEvents: &ast.RenameSpanEvents{ + EventNameMap: map[string]string{ + "exception.stacktrace": "exception.stack_trace", + }, }, }, - }, - { - RenameAttributes: &ast.RenameSpanEventAttributes{ - ApplyToEvents: []types.EventName{"exception.stack_trace"}, - AttributeMap: ast.AttributeMap{ - "peer.service": "peer.service.name", + { + RenameAttributes: &ast.RenameSpanEventAttributes{ + ApplyToEvents: []types.EventName{"exception.stack_trace"}, + AttributeMap: ast.AttributeMap{ + "peer.service": "peer.service.name", + }, }, }, }, }, - }, - Logs: ast.Logs{Changes: []ast.LogsChange{ - {RenameAttributes: &ast.RenameAttributes{ - AttributeMap: map[string]string{ - "process.executable_name": "process.executable.name", - }, - }}, - }}, - - Metrics: ast.Metrics{ - Changes: []ast.MetricsChange{ - { - RenameAttributes: &ast.AttributeMapForMetrics{ - AttributeMap: map[string]string{ - "http.status_code": "http.response_status_code", + Logs: ast.Logs{ + Changes: []ast.LogsChange{ + { + RenameAttributes: &ast.RenameAttributes{ + AttributeMap: map[string]string{ + "process.executable_name": "process.executable.name", + }, }, - }}, - { - RenameMetrics: map[types.MetricName]types.MetricName{ - "container.cpu.usage.total": "cpu.usage.total", - "container.memory.usage.max": "memory.usage.max", }, }, - { - RenameAttributes: &ast.AttributeMapForMetrics{ - ApplyToMetrics: []types.MetricName{ - "system.cpu.utilization", - "system.memory.usage", - "system.memory.utilization", - "system.paging.usage", + }, + + Metrics: ast.Metrics{ + Changes: []ast.MetricsChange{ + { + RenameAttributes: &ast.AttributeMapForMetrics{ + AttributeMap: map[string]string{ + "http.status_code": "http.response_status_code", + }, }, - AttributeMap: map[string]string{ - "status": "state", + }, + { + RenameMetrics: map[types.MetricName]types.MetricName{ + "container.cpu.usage.total": "cpu.usage.total", + "container.memory.usage.max": "memory.usage.max", + }, + }, + { + RenameAttributes: &ast.AttributeMapForMetrics{ + ApplyToMetrics: []types.MetricName{ + "system.cpu.utilization", + "system.memory.usage", + "system.memory.utilization", + "system.paging.usage", + }, + AttributeMap: map[string]string{ + "status": "state", + }, }, }, }, }, }, }, - }, - }, ts) + }, ts, + ) } func TestFailParseSchemaFile(t *testing.T) { @@ -167,15 +176,3 @@ func TestFailParseSchema(t *testing.T) { _, err = Parse(bytes.NewReader([]byte("file_format: 1.0.0"))) assert.Error(t, err) } - -func TestCheckFileFormatField(t *testing.T) { - // Invalid file format version numbers. - assert.Error(t, checkFileFormatField("not a semver")) - assert.Error(t, checkFileFormatField("2.0.0")) - assert.Error(t, checkFileFormatField("1.1.0")) - - // Valid cases. - assert.NoError(t, checkFileFormatField("1.0.0")) - assert.NoError(t, checkFileFormatField("1.0.1")) - assert.NoError(t, checkFileFormatField("1.0.10000-alpha+4857")) -} diff --git a/schema/v1.0/testdata/unsupported-file-format.yaml b/schema/v1.0/testdata/unsupported-file-format.yaml index fb24f4861a8..2348606bcb7 100644 --- a/schema/v1.0/testdata/unsupported-file-format.yaml +++ b/schema/v1.0/testdata/unsupported-file-format.yaml @@ -1,4 +1,5 @@ file_format: 1.1.0 +schema_url: https://opentelemetry.io/schemas/1.1.0 versions: 1.1.0: diff --git a/schema/v1.1/ast/ast_schema.go b/schema/v1.1/ast/ast_schema.go new file mode 100644 index 00000000000..9a62fc4297c --- /dev/null +++ b/schema/v1.1/ast/ast_schema.go @@ -0,0 +1,50 @@ +// Copyright The OpenTelemetry Authors +// +// 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. + +package ast // import "go.opentelemetry.io/otel/schema/v1.1/ast" + +import ( + ast10 "go.opentelemetry.io/otel/schema/v1.0/ast" + "go.opentelemetry.io/otel/schema/v1.1/types" +) + +// Schema represents a Schema file in FileFormat 1.1.0 as defined in +// https://github.com/open-telemetry/oteps/blob/main/text/0152-telemetry-schemas.md +type Schema struct { + // Schema file format. SHOULD be 1.1.0 for the current specification version. + // See https://github.com/open-telemetry/oteps/blob/main/text/0152-telemetry-schemas.md#schema-file-format-number + FileFormat string `yaml:"file_format"` + + // Schema URL is an identifier of a Schema. The URL specifies a location of this + // Schema File that can be retrieved (so it is a URL and not just a URI) using HTTP + // or HTTPS protocol. + // See https://github.com/open-telemetry/oteps/blob/main/text/0152-telemetry-schemas.md#schema-url + SchemaURL string `yaml:"schema_url"` + + // Versions section that lists changes that happened in each particular version. + Versions map[types.TelemetryVersion]VersionDef +} + +// VersionDef corresponds to a section representing one version under the "versions" +// top-level key. +// Note that most of the fields are the same as in ast 1.0 package, only Metrics are defined +// differently, since only that field has changed from 1.0 to 1.1 of schema file format. +type VersionDef struct { + All ast10.Attributes + Resources ast10.Attributes + Spans ast10.Spans + SpanEvents ast10.SpanEvents `yaml:"span_events"` + Logs ast10.Logs + Metrics Metrics +} diff --git a/schema/v1.1/ast/metrics.go b/schema/v1.1/ast/metrics.go new file mode 100644 index 00000000000..0d0e3eacb38 --- /dev/null +++ b/schema/v1.1/ast/metrics.go @@ -0,0 +1,52 @@ +// Copyright The OpenTelemetry Authors +// +// 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. + +package ast // import "go.opentelemetry.io/otel/schema/v1.1/ast" + +import ( + ast10 "go.opentelemetry.io/otel/schema/v1.0/ast" + types10 "go.opentelemetry.io/otel/schema/v1.0/types" + types11 "go.opentelemetry.io/otel/schema/v1.1/types" +) + +// Metrics corresponds to a section representing a list of changes that happened +// to metrics schema in a particular version. +type Metrics struct { + Changes []MetricsChange +} + +// MetricsChange corresponds to a section representing metrics change. +type MetricsChange struct { + RenameMetrics map[types10.MetricName]types10.MetricName `yaml:"rename_metrics"` + RenameAttributes *ast10.AttributeMapForMetrics `yaml:"rename_attributes"` + Split *SplitMetric `yaml:"split"` +} + +// SplitMetric corresponds to a section representing a splitting of a metric +// into multiple metrics by eliminating an attribute. +// SplitMetrics is introduced in schema file format 1.1, +// see https://github.com/open-telemetry/opentelemetry-specification/pull/2653 +type SplitMetric struct { + // Name of the old metric to split. + ApplyToMetric types10.MetricName `yaml:"apply_to_metric"` + + // Name of attribute in the old metric to use for splitting. The attribute will be + // eliminated, the new metric will not have it. + ByAttribute types11.AttributeName `yaml:"by_attribute"` + + // Names of new metrics to create, one for each possible value of attribute. + // map of key/values. The keys are the new metric name starting from this version, + // the values are old attribute value used in the previous version. + MetricsFromAttributes map[types10.MetricName]types11.AttributeValue `yaml:"metrics_from_attributes"` +} diff --git a/schema/v1.1/parser.go b/schema/v1.1/parser.go new file mode 100644 index 00000000000..7badc1b84fa --- /dev/null +++ b/schema/v1.1/parser.go @@ -0,0 +1,62 @@ +// Copyright The OpenTelemetry Authors +// +// 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. + +package schema // import "go.opentelemetry.io/otel/schema/v1.1" + +import ( + "io" + "os" + + "gopkg.in/yaml.v2" + + "go.opentelemetry.io/otel/schema/internal" + "go.opentelemetry.io/otel/schema/v1.1/ast" +) + +// Major file version number that this library supports. +const supportedFormatMajor = 1 + +// Maximum minor version number that this library supports. +const supportedFormatMinor = 1 + +// ParseFile a schema file. schemaFilePath is the file path. +func ParseFile(schemaFilePath string) (*ast.Schema, error) { + file, err := os.Open(schemaFilePath) + if err != nil { + return nil, err + } + return Parse(file) +} + +// Parse a schema file. schemaFileContent is the readable content of the schema file. +func Parse(schemaFileContent io.Reader) (*ast.Schema, error) { + var ts ast.Schema + d := yaml.NewDecoder(schemaFileContent) + err := d.Decode(&ts) + if err != nil { + return nil, err + } + + err = internal.CheckFileFormatField(ts.FileFormat, supportedFormatMajor, supportedFormatMinor) + if err != nil { + return nil, err + } + + err = internal.CheckSchemaURL(ts.SchemaURL) + if err != nil { + return nil, err + } + + return &ts, nil +} diff --git a/schema/v1.1/parser_test.go b/schema/v1.1/parser_test.go new file mode 100644 index 00000000000..5f02a52b225 --- /dev/null +++ b/schema/v1.1/parser_test.go @@ -0,0 +1,174 @@ +// Copyright The OpenTelemetry Authors +// +// 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. + +package schema + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + ast10 "go.opentelemetry.io/otel/schema/v1.0/ast" + types10 "go.opentelemetry.io/otel/schema/v1.0/types" + ast11 "go.opentelemetry.io/otel/schema/v1.1/ast" + types11 "go.opentelemetry.io/otel/schema/v1.1/types" +) + +func TestParseSchemaFile(t *testing.T) { + ts, err := ParseFile("testdata/valid-example.yaml") + assert.NoError(t, err) + assert.NotNil(t, ts) + assert.EqualValues( + t, &ast11.Schema{ + FileFormat: "1.1.0", + SchemaURL: "https://opentelemetry.io/schemas/1.1.0", + Versions: map[types11.TelemetryVersion]ast11.VersionDef{ + "1.0.0": {}, + + "1.1.0": { + All: ast10.Attributes{ + Changes: []ast10.AttributeChange{ + { + RenameAttributes: &ast10.AttributeMap{ + "k8s.cluster.name": "kubernetes.cluster.name", + "k8s.namespace.name": "kubernetes.namespace.name", + "k8s.node.name": "kubernetes.node.name", + "k8s.node.uid": "kubernetes.node.uid", + "k8s.pod.name": "kubernetes.pod.name", + "k8s.pod.uid": "kubernetes.pod.uid", + "k8s.container.name": "kubernetes.container.name", + "k8s.replicaset.name": "kubernetes.replicaset.name", + "k8s.replicaset.uid": "kubernetes.replicaset.uid", + "k8s.cronjob.name": "kubernetes.cronjob.name", + "k8s.cronjob.uid": "kubernetes.cronjob.uid", + "k8s.job.name": "kubernetes.job.name", + "k8s.job.uid": "kubernetes.job.uid", + "k8s.statefulset.name": "kubernetes.statefulset.name", + "k8s.statefulset.uid": "kubernetes.statefulset.uid", + "k8s.daemonset.name": "kubernetes.daemonset.name", + "k8s.daemonset.uid": "kubernetes.daemonset.uid", + "k8s.deployment.name": "kubernetes.deployment.name", + "k8s.deployment.uid": "kubernetes.deployment.uid", + "service.namespace": "service.namespace.name", + }, + }, + }, + }, + + Resources: ast10.Attributes{ + Changes: []ast10.AttributeChange{ + { + RenameAttributes: &ast10.AttributeMap{ + "telemetry.auto.version": "telemetry.auto_instr.version", + }, + }, + }, + }, + + Spans: ast10.Spans{ + Changes: []ast10.SpansChange{ + { + RenameAttributes: &ast10.AttributeMapForSpans{ + AttributeMap: ast10.AttributeMap{ + "peer.service": "peer.service.name", + }, + ApplyToSpans: []types10.SpanName{"HTTP GET"}, + }, + }, + }, + }, + + SpanEvents: ast10.SpanEvents{ + Changes: []ast10.SpanEventsChange{ + { + RenameEvents: &ast10.RenameSpanEvents{ + EventNameMap: map[string]string{ + "exception.stacktrace": "exception.stack_trace", + }, + }, + }, + { + RenameAttributes: &ast10.RenameSpanEventAttributes{ + ApplyToEvents: []types10.EventName{"exception.stack_trace"}, + AttributeMap: ast10.AttributeMap{ + "peer.service": "peer.service.name", + }, + }, + }, + }, + }, + + Logs: ast10.Logs{ + Changes: []ast10.LogsChange{ + { + RenameAttributes: &ast10.RenameAttributes{ + AttributeMap: map[string]string{ + "process.executable_name": "process.executable.name", + }, + }, + }, + }, + }, + + Metrics: ast11.Metrics{ + Changes: []ast11.MetricsChange{ + { + RenameAttributes: &ast10.AttributeMapForMetrics{ + AttributeMap: map[string]string{ + "http.status_code": "http.response_status_code", + }, + }, + }, + { + RenameMetrics: map[types10.MetricName]types10.MetricName{ + "container.cpu.usage.total": "cpu.usage.total", + "container.memory.usage.max": "memory.usage.max", + }, + }, + { + RenameAttributes: &ast10.AttributeMapForMetrics{ + ApplyToMetrics: []types10.MetricName{ + "system.cpu.utilization", + "system.memory.usage", + "system.memory.utilization", + "system.paging.usage", + }, + AttributeMap: map[string]string{ + "status": "state", + }, + }, + }, + { + Split: &ast11.SplitMetric{ + ApplyToMetric: "system.paging.operations", + ByAttribute: "direction", + MetricsFromAttributes: map[types10.MetricName]types11.AttributeValue{ + "system.paging.operations.in": "in", + "system.paging.operations.out": "out", + }, + }, + }, + }, + }, + }, + }, + }, ts, + ) +} + +func TestFailParseSchemaFile(t *testing.T) { + ts, err := ParseFile("testdata/unsupported-file-format.yaml") + assert.Error(t, err) + assert.Nil(t, ts) +} diff --git a/schema/v1.1/testdata/unsupported-file-format.yaml b/schema/v1.1/testdata/unsupported-file-format.yaml new file mode 100644 index 00000000000..47d30c024fd --- /dev/null +++ b/schema/v1.1/testdata/unsupported-file-format.yaml @@ -0,0 +1,10 @@ +file_format: 1.2.0 +schema_url: https://opentelemetry.io/schemas/1.1.0 + +versions: + 1.1.0: + all: + changes: + - rename_attributes: + k8s.cluster.name: kubernetes.cluster.name + 1.0.0: diff --git a/schema/v1.1/testdata/valid-example.yaml b/schema/v1.1/testdata/valid-example.yaml new file mode 100644 index 00000000000..138f5628cad --- /dev/null +++ b/schema/v1.1/testdata/valid-example.yaml @@ -0,0 +1,144 @@ +file_format: 1.1.0 + +schema_url: https://opentelemetry.io/schemas/1.1.0 + +versions: + 1.1.0: + # Section "all" applies to attribute names for all data types: resources, spans, logs, + # span events, metric labels. + # + # The translations in "all" section are performed first (for each particular version). + # Only after that the translations in the specific section ("resources", "traces", + # "metrics" or "logs") that corresponds to the data type are applied. + # + # The only translation possible in section "all" is renaming of attributes in + # versions. For human readability versions are listed in reverse chronological + # order, however note that the translations are applied in the order defined by + # semver ordering. + all: + changes: + - rename_attributes: + # Mapping of attribute names (label names for metrics). The key is the old name + # used prior to this version, the value is the new name starting from this version. + + # Rename k8s.* to kubernetes.* + k8s.cluster.name: kubernetes.cluster.name + k8s.namespace.name: kubernetes.namespace.name + k8s.node.name: kubernetes.node.name + k8s.node.uid: kubernetes.node.uid + k8s.pod.name: kubernetes.pod.name + k8s.pod.uid: kubernetes.pod.uid + k8s.container.name: kubernetes.container.name + k8s.replicaset.name: kubernetes.replicaset.name + k8s.replicaset.uid: kubernetes.replicaset.uid + k8s.cronjob.name: kubernetes.cronjob.name + k8s.cronjob.uid: kubernetes.cronjob.uid + k8s.job.name: kubernetes.job.name + k8s.job.uid: kubernetes.job.uid + k8s.statefulset.name: kubernetes.statefulset.name + k8s.statefulset.uid: kubernetes.statefulset.uid + k8s.daemonset.name: kubernetes.daemonset.name + k8s.daemonset.uid: kubernetes.daemonset.uid + k8s.deployment.name: kubernetes.deployment.name + k8s.deployment.uid: kubernetes.deployment.uid + + service.namespace: service.namespace.name + + # Like "all" the "resources" section may contain only attribute renaming translations. + # The only translation possible in this section is renaming of attributes in + # versions. + resources: + changes: + - rename_attributes: + # Mapping of attribute names. The key is the old name + # used prior to this version, the value is the new name starting from this version. + telemetry.auto.version: telemetry.auto_instr.version + + spans: + changes: + # Sequence of translations to apply to convert the schema from a prior version + # to this version. The order in this sequence is important. Translations are + # applied from top to bottom in the listed order. + - rename_attributes: + # Rename attributes of all spans, regardless of span name. + # The keys are the old attribute name used prior to this version, the values are + # the new attribute name starting from this version. + attribute_map: + peer.service: peer.service.name + apply_to_spans: + # apply only to spans named "HTTP GET" + - "HTTP GET" + span_events: + changes: + # Sequence of translations to apply to convert the schema from a prior version + # to this version. The order in this sequence is important. Translations are + # applied from top to bottom in the listed order. + - rename_events: + # Rename events. The keys are old event names, the values are the new event names. + name_map: {exception.stacktrace: exception.stack_trace} + + - rename_attributes: + # Rename attributes of events. + # The keys are the old attribute name used prior to this version, the values are + # the new attribute name starting from this version. + attribute_map: + peer.service: peer.service.name + + apply_to_events: + # Optional event names to apply to. If empty applies to all events. + # Conditions in apply_to_spans and apply_to_events are logical AND-ed, + # both should match for transformation to be applied. + - exception.stack_trace + + metrics: + changes: + # Sequence of translations to apply to convert the schema from a prior version + # to this version. The order in this sequence is important. Translations are + # applied from top to bottom in the listed order. + + - rename_attributes: + # Rename attributes of all metrics, regardless of metric name. + # The keys are the old attribute name used prior to this version, the values are + # the new attribute name starting from this version. + attribute_map: + http.status_code: http.response_status_code + + - rename_metrics: + # Rename metrics. The keys are old metric names, the values are the new metric names. + container.cpu.usage.total: cpu.usage.total + container.memory.usage.max: memory.usage.max + + - rename_attributes: + apply_to_metrics: + # Name of the metric to apply this rule to. If empty the rule applies to all metrics. + - system.cpu.utilization + - system.memory.usage + - system.memory.utilization + - system.paging.usage + attribute_map: + # The keys are the old attribute name used prior to this version, the values are + # the new attribute name starting from this version. + status: state + + - split: + # Rules to split a metric into several metrics using an attribute for split. + # This example rule implements the change done by + # https://github.com/open-telemetry/opentelemetry-specification/pull/2617 + # Name of old metric to split. + apply_to_metric: system.paging.operations + # Name of attribute in the old metric to use for splitting. The attribute will be + # eliminated, the new metric will not have it. + by_attribute: direction + # Names of new metrics to create, one for each possible value of the attribute. + metrics_from_attributes: + # If "direction" attribute equals "in" create a new metric called "system.paging.operations.in". + system.paging.operations.in: in + system.paging.operations.out: out + + logs: + changes: + - rename_attributes: + attribute_map: + process.executable_name: process.executable.name + + 1.0.0: diff --git a/schema/v1.1/types/types.go b/schema/v1.1/types/types.go new file mode 100644 index 00000000000..1f703f57daf --- /dev/null +++ b/schema/v1.1/types/types.go @@ -0,0 +1,26 @@ +// Copyright The OpenTelemetry Authors +// +// 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. + +package types // import "go.opentelemetry.io/otel/schema/v1.1/types" + +import types10 "go.opentelemetry.io/otel/schema/v1.0/types" + +// TelemetryVersion is a version number key in the schema file (e.g. "1.7.0"). +type TelemetryVersion types10.TelemetryVersion + +// AttributeName is an attribute name string. +type AttributeName string + +// AttributeValue is an attribute value. +type AttributeValue interface{}