Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attribute requirement levels #92

Merged
merged 16 commits into from
May 31, 2022
6 changes: 6 additions & 0 deletions semantic-conventions/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

Please update the changelog as part of any significant pull request.

## Unreleased

- BREAKING: Introduced attribute requirement levels ([#92](https://github.com/open-telemetry/build-tools/pull/92)):
- Schema: Attribute property `required` is removed and replaced by `requirement_level`, supported values are changed to `required` (previously `always`), `conditionally_required` (previously `conditional`), `recommended`, and `optional`.
- Templates: `opentelemetry.semconv.model.semantic_attribute.Required` enum is replaced by `RequirementLevel` with supported values listed above, `required_msg` is renamed to `requirement_level_msg`

## v0.8.0

- Add `name` field for events. It defaults to the `prefix`
Expand Down
2 changes: 1 addition & 1 deletion semantic-conventions/dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
black==21.8b0
black==22.3.0
mypy==0.910
pytest==6.2.5
flake8==3.9.2
Expand Down
35 changes: 27 additions & 8 deletions semantic-conventions/semconv.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -291,26 +291,45 @@
"allOf": [
{
"properties": {
"required": {
"description": "specifies if the attribute is mandatory. Can be 'always', or 'conditional'. When omitted, the attribute is not required. When set to 'conditional',the string provided as <condition> MUST specify the conditions under which the attribute is required.",
"requirement_level": {
"description": "specifies the attribute requirement level. Can be 'required', 'conditionally_required', 'recommended', or 'optional'. When omitted, the attribute is 'recommended'. When set to 'conditionally_required', the string provided as <condition> MUST specify the conditions under which the attribute is required.",
"oneOf": [
{
"type": "string",
"enum": [
"always"
]
"const": "required"
},
{
"type": "object",
"additionalProperties": false,
"required": [
"conditional"
"conditionally_required"
],
"properties": {
"conditional": {
"condition": {
"type": "string"
}
}
},
{
"oneOf": [
{
"const": "recommended"
},
{
"type": "object",
"additionalProperties": false,
"required": [
"recommended"
],
"properties": {
"recommended": {
"type": "string"
}
}
}
]
},
{
"const": "optional"
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@
)


class Required(Enum):
ALWAYS = 1
CONDITIONAL = 2
NO = 3
class RequirementLevel(Enum):
REQUIRED = 1
CONDITIONALLY_REQUIRED = 2
RECOMMENDED = 3
OPTIONAL = 4


class StabilityLevel(Enum):
Expand Down Expand Up @@ -59,8 +60,8 @@ class SemanticAttribute:
tag: str
stability: StabilityLevel
deprecated: str
required: Required
required_msg: str
requirement_level: RequirementLevel
requirement_level_msg: str
sampling_relevant: bool
note: str
position: List[int]
Expand Down Expand Up @@ -98,7 +99,7 @@ def parse(
"tag",
"deprecated",
"stability",
"required",
"requirement_level",
"sampling_relevant",
"note",
)
Expand Down Expand Up @@ -133,32 +134,50 @@ def parse(
fqn = ref

required_value_map = {
"always": Required.ALWAYS,
"conditional": Required.CONDITIONAL,
"": Required.NO,
"required": RequirementLevel.REQUIRED,
"conditionally_required": RequirementLevel.CONDITIONALLY_REQUIRED,
"": RequirementLevel.RECOMMENDED,
"recommended": RequirementLevel.RECOMMENDED,
"optional": RequirementLevel.OPTIONAL,
}
required_msg = ""
required_val = attribute.get("required", "")
required: Optional[Required]
if isinstance(required_val, CommentedMap):
required = Required.CONDITIONAL
required_msg = required_val.get("conditional", None)
if required_msg is None:
position = position_data["required"]
msg = "Missing message for conditional required field!"
requirement_level_msg = ""
requirement_level_val = attribute.get("requirement_level", "")
requirement_level: Optional[RequirementLevel]
if isinstance(requirement_level_val, CommentedMap):
lmolkova marked this conversation as resolved.
Show resolved Hide resolved

if len(requirement_level_val) != 1:
position = position_data["requirement_level"]
msg = "Multiple requirement_level values are not allowed!"
raise ValidationError.from_yaml_pos(position, msg)

recommended_msg = requirement_level_val.get("recommended", None)
condition_msg = requirement_level_val.get(
"conditionally_required", None
)
if condition_msg is not None:
requirement_level = RequirementLevel.CONDITIONALLY_REQUIRED
requirement_level_msg = condition_msg
elif recommended_msg is not None:
requirement_level = RequirementLevel.RECOMMENDED
requirement_level_msg = recommended_msg
else:
required = required_value_map.get(required_val)
if required == Required.CONDITIONAL:
position = position_data["required"]
msg = "Missing message for conditional required field!"
raise ValidationError.from_yaml_pos(position, msg)
if required is None:
position = position_data["required"]
requirement_level = required_value_map.get(requirement_level_val)

if requirement_level is None:
position = position_data["requirement_level"]
msg = "Value '{}' for required field is not allowed".format(
required_val
requirement_level_val
)
raise ValidationError.from_yaml_pos(position, msg)

if (
requirement_level == RequirementLevel.CONDITIONALLY_REQUIRED
and not requirement_level_msg
):
position = position_data["requirement_level"]
msg = "Missing message for conditionally required field!"
raise ValidationError.from_yaml_pos(position, msg)

tag = attribute.get("tag", "").strip()
stability, deprecated = SemanticAttribute.parse_stability_deprecated(
attribute.get("stability"), attribute.get("deprecated"), position_data
Expand Down Expand Up @@ -196,8 +215,8 @@ def parse(
tag=tag,
deprecated=deprecated,
stability=stability,
required=required,
required_msg=str(required_msg).strip(),
requirement_level=requirement_level,
requirement_level_msg=str(requirement_level_msg).strip(),
sampling_relevant=sampling_relevant,
note=parsed_note,
position=position,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from opentelemetry.semconv.model.constraints import AnyOf, Include, parse_constraints
from opentelemetry.semconv.model.exceptions import ValidationError
from opentelemetry.semconv.model.semantic_attribute import (
Required,
RequirementLevel,
SemanticAttribute,
unique_attributes,
)
Expand Down Expand Up @@ -150,17 +150,21 @@ def all_attributes(self):

def sampling_attributes(self):
return unique_attributes(
[attr for attr in self.attributes if attr.sampling_relevant]
attr for attr in self.attributes if attr.sampling_relevant
)

def required_attributes(self):
return unique_attributes(
[attr for attr in self.attributes if attr.required == Required.ALWAYS]
attr
for attr in self.attributes
if attr.requirement_level == RequirementLevel.REQUIRED
)

def conditional_attributes(self):
return unique_attributes(
[attr for attr in self.attributes if attr.required == Required.CONDITIONAL]
attr
for attr in self.attributes
if attr.requirement_level == RequirementLevel.CONDITIONALLY_REQUIRED
)

def any_of(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
import mistune
from jinja2 import Environment, FileSystemLoader, select_autoescape

from opentelemetry.semconv.model.semantic_attribute import Required, TextWithLinks
from opentelemetry.semconv.model.semantic_attribute import (
RequirementLevel,
TextWithLinks,
)
from opentelemetry.semconv.model.semantic_convention import SemanticConventionSet
from opentelemetry.semconv.model.utils import ID_RE

Expand Down Expand Up @@ -233,12 +236,12 @@ def render(
template = env.get_template(file_name, globals=data)
template.globals["now"] = datetime.datetime.utcnow()
template.globals["version"] = os.environ.get("ARTIFACT_VERSION", "dev")
template.globals["Required"] = Required
template.globals["RequirementLevel"] = RequirementLevel
template.stream(data).dump(output_name)
else:
data = self.get_data_single_file(semconvset, template_path)
template = env.get_template(file_name, globals=data)
template.globals["now"] = datetime.datetime.utcnow()
template.globals["version"] = os.environ.get("ARTIFACT_VERSION", "dev")
template.globals["Required"] = Required
template.globals["RequirementLevel"] = RequirementLevel
template.stream(data).dump(output_file)
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from opentelemetry.semconv.model.semantic_attribute import (
EnumAttributeType,
EnumMember,
Required,
RequirementLevel,
SemanticAttribute,
StabilityLevel,
)
Expand Down Expand Up @@ -69,7 +69,7 @@ class MarkdownRenderer:
valid_parameters = ["tag", "full", "remove_constraints"]

prelude = "<!-- semconv {} -->\n"
table_headers = "| Attribute | Type | Description | Examples | Required |\n|---|---|---|---|---|\n"
table_headers = "| Attribute | Type | Description | Examples | Requirement Level |\n|---|---|---|---|---|\n"

def __init__(
self, md_folder, semconvset: SemanticConventionSet, options=MarkdownOptions()
Expand Down Expand Up @@ -150,24 +150,36 @@ def to_markdown_attr(
)
else:
examples = "; ".join("`{}`".format(ex) for ex in example_list)
if attribute.required == Required.ALWAYS:
required = "Yes"
elif attribute.required == Required.CONDITIONAL:
if len(attribute.required_msg) < self.options.break_count:
required = attribute.required_msg
if attribute.requirement_level == RequirementLevel.REQUIRED:
required = "Required"
elif attribute.requirement_level == RequirementLevel.CONDITIONALLY_REQUIRED:
if len(attribute.requirement_level_msg) < self.options.break_count:
required = "Conditionally Required: " + attribute.requirement_level_msg
else:
# We put the condition in the notes after the table
self.render_ctx.add_note(attribute.required_msg)
required = "Conditional [{}]".format(len(self.render_ctx.notes))
else:
# check if they are required by some constraint
self.render_ctx.add_note(attribute.requirement_level_msg)
required = "Conditionally Required: [{}]".format(
len(self.render_ctx.notes)
)
elif attribute.requirement_level == RequirementLevel.OPTIONAL:
required = "Optional"
else: # attribute.requirement_level == Required.RECOMMENDED
# check if there are any notes
if (
not self.render_ctx.is_remove_constraint
and self.render_ctx.current_semconv.has_attribute_constraint(attribute)
):
required = "See below"
else:
required = "No"
if not attribute.requirement_level_msg:
required = "Recommended"
elif len(attribute.requirement_level_msg) < self.options.break_count:
required = "Recommended: " + attribute.requirement_level_msg
else:
# We put the condition in the notes after the table
self.render_ctx.add_note(attribute.requirement_level_msg)
required = "Recommended: [{}]".format(len(self.render_ctx.notes))

output.write(
"| {} | {} | {} | {} | {} |\n".format(
name, attr_type, description, examples, required
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

<!-- Re-generate TOC with `TODO: ADD cmd` -->
<!-- semconv http -->
| Attribute | Type | Description | Examples | Required |
| Attribute | Type | Description | Examples | Requirement Level |
|---|---|---|---|---|
| `http.method` | string | HTTP request method. | `GET`; `POST`; `HEAD` | Yes |
| `http.url` | string | Full HTTP request URL in the form `scheme://host[:port]/path?query[#fragment]`. Usually the fragment is not transmitted over HTTP, but if it is known, it should be included nevertheless. | `https://www.foo.bar/search?q=OpenTelemetry#SemConv` | No |
| `http.target` | string | The full request target as passed in a HTTP request line or equivalent. | `/path/12314/?q=ddds#123` | No |
| `http.host` | string | The value of the [HTTP host header](https://tools.ietf.org/html/rfc7230#section-5.4). When the header is empty or not present, this attribute should be the same. | `www.example.org` | No |
| `http.scheme` | string | The URI scheme identifying the used protocol. | `http`; `https` | No |
| `http.status_code` | int | [HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6). | `200` | If and only if one was received/sent |
| `http.status_text` | string | **Deprecated: Use attribute `status_description` instead.**<br>[HTTP reason phrase](https://tools.ietf.org/html/rfc7230#section-3.1.2). | `OK` | No |
| `http.flavor` | string | **Deprecated. Use attribute `flavor_new` instead.**<br>Kind of HTTP protocol used [1] | `1.0` | No |
| `http.user_agent` | string | Value of the [HTTP User-Agent](https://tools.ietf.org/html/rfc7231#section-5.5.3) header sent by the client. | `CERN-LineMode/2.15 libwww/2.17b3` | No |
| `http.method` | string | HTTP request method. | `GET`; `POST`; `HEAD` | Required |
| `http.url` | string | Full HTTP request URL in the form `scheme://host[:port]/path?query[#fragment]`. Usually the fragment is not transmitted over HTTP, but if it is known, it should be included nevertheless. | `https://www.foo.bar/search?q=OpenTelemetry#SemConv` | Recommended |
| `http.target` | string | The full request target as passed in a HTTP request line or equivalent. | `/path/12314/?q=ddds#123` | Recommended |
| `http.host` | string | The value of the [HTTP host header](https://tools.ietf.org/html/rfc7230#section-5.4). When the header is empty or not present, this attribute should be the same. | `www.example.org` | Recommended |
| `http.scheme` | string | The URI scheme identifying the used protocol. | `http`; `https` | Recommended |
| `http.status_code` | int | [HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6). | `200` | Conditionally Required: if and only if one was received/sent |
| `http.status_text` | string | **Deprecated: Use attribute `status_description` instead.**<br>[HTTP reason phrase](https://tools.ietf.org/html/rfc7230#section-3.1.2). | `OK` | Recommended |
| `http.flavor` | string | **Deprecated. Use attribute `flavor_new` instead.**<br>Kind of HTTP protocol used [1] | `1.0` | Recommended |
| `http.user_agent` | string | Value of the [HTTP User-Agent](https://tools.ietf.org/html/rfc7231#section-5.5.3) header sent by the client. | `CERN-LineMode/2.15 libwww/2.17b3` | Recommended |

**[1]:** If `net.transport` is not specified, it can be assumed to be `IP.TCP` except if `http.flavor` is `QUIC`, in which case `IP.UDP` is assumed.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ groups:
attributes:
- id: method
type: string
required: always
requirement_level: required
sampling_relevant: false
brief: 'HTTP request method.'
examples: ["GET", "POST", "HEAD"]
Expand All @@ -35,8 +35,8 @@ groups:
examples: ["http", "https"]
- id: status_code
type: int
required:
conditional: "If and only if one was received/sent"
requirement_level:
conditionally_required: "if and only if one was received/sent"
brief: '[HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6).'
examples: [200]
- id: status_text
Expand Down Expand Up @@ -96,8 +96,8 @@ groups:
attributes:
- id: server_name
type: string
required:
conditional: >
requirement_level:
conditionally_required: >
This should be obtained via configuration.
sampling_relevant: false
brief: >
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@

| Attribute name | Notes and examples | Required? |
| :------------- | :----------------------------------------------------------- | --------- |
| `http.method` | HTTP request method. E.g. `"GET"`. | Yes |
| `http.method` | HTTP request method. E.g. `"GET"`. | Required |
| `http.url` | Full HTTP request URL in the form `scheme://host[:port]/path?query[#fragment]`. Usually the fragment is not transmitted over HTTP, but if it is known, it should be included nevertheless. | Defined later. |
| `http.target` | The full request target as passed in a [HTTP request line][] or equivalent, e.g. `"/path/12314/?q=ddds#123"`. | Defined later. |
| `http.host` | The value of the [HTTP host header][]. When the header is empty or not present, this attribute should be the same. | Defined later. |
| `http.scheme` | The URI scheme identifying the used protocol: `"http"` or `"https"` | Defined later. |
| `http.status_code` | [HTTP response status code][]. E.g. `200` (integer) | If and only if one was received/sent. |
| `http.status_text` | [HTTP reason phrase][]. E.g. `"OK"` | No |
| `http.status_code` | [HTTP response status code][]. E.g. `200` (integer) | Conditionally Required: if and only if one was received/sent. |
| `http.status_text` | [HTTP reason phrase][]. E.g. `"OK"` | Recommended |
| `http.flavor` | Kind of HTTP protocol used: `"1.0"`, `"1.1"`, `"2"`, `"SPDY"` or `"QUIC"`. | No |
| `http.user_agent` | Value of the HTTP [User-Agent][] header sent by the client. | No |
| `http.user_agent` | Value of the HTTP [User-Agent][] header sent by the client. | Recommended |

<!-- endsemconv -->

Expand Down
Loading