Skip to content

Commit

Permalink
Introduce "split" metric schema transformation
Browse files Browse the repository at this point in the history
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:
open-telemetry/opentelemetry-specification#2617

This PR implements specification change open-telemetry/opentelemetry-specification#2653

This PR creates package v1.1 for the new functionality. The old package v1.0
remains unchanged.
  • Loading branch information
tigrannajaryan committed Jul 11, 2022
1 parent eb9e058 commit a66d449
Show file tree
Hide file tree
Showing 12 changed files with 620 additions and 58 deletions.
4 changes: 2 additions & 2 deletions schema/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
73 changes: 73 additions & 0 deletions schema/internal/parser_checks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// 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 (
"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 fmt.Errorf("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
}
56 changes: 6 additions & 50 deletions schema/v1.0/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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)
Expand All @@ -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
}
13 changes: 7 additions & 6 deletions schema/v1.0/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

"github.com/stretchr/testify/assert"

"go.opentelemetry.io/otel/schema/internal"
"go.opentelemetry.io/otel/schema/v1.0/ast"
"go.opentelemetry.io/otel/schema/v1.0/types"
)
Expand Down Expand Up @@ -170,12 +171,12 @@ func TestFailParseSchema(t *testing.T) {

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"))
assert.Error(t, internal.CheckFileFormatField("not a semver", supportedFormatMajor, supportedFormatMinor))
assert.Error(t, internal.CheckFileFormatField("2.0.0", supportedFormatMajor, supportedFormatMinor))
assert.Error(t, internal.CheckFileFormatField("1.1.0", supportedFormatMajor, supportedFormatMinor))

// 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"))
assert.NoError(t, internal.CheckFileFormatField("1.0.0", supportedFormatMajor, supportedFormatMinor))
assert.NoError(t, internal.CheckFileFormatField("1.0.1", supportedFormatMajor, supportedFormatMinor))
assert.NoError(t, internal.CheckFileFormatField("1.0.10000-alpha+4857", supportedFormatMajor, supportedFormatMinor))
}
1 change: 1 addition & 0 deletions schema/v1.0/testdata/unsupported-file-format.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
file_format: 1.1.0
schema_url: https://opentelemetry.io/schemas/1.1.0

versions:
1.1.0:
Expand Down
50 changes: 50 additions & 0 deletions schema/v1.1/ast/ast_schema.go
Original file line number Diff line number Diff line change
@@ -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
}
52 changes: 52 additions & 0 deletions schema/v1.1/ast/metrics.go
Original file line number Diff line number Diff line change
@@ -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.
AttributesToMetrics map[types10.MetricName]types11.AttributeValue `yaml:"attributes_to_metrics"`
}
62 changes: 62 additions & 0 deletions schema/v1.1/parser.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit a66d449

Please sign in to comment.