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

feat: Rework Meter and MeterProvider API #1442

Conversation

elias19r
Copy link
Contributor

@elias19r elias19r commented Apr 27, 2023

  • Remove argument validations from API and add doc comments:
    validation should be performed by the implementation of the API
  • Add new parent classes SynchronousInstrument and AsynchronousInstrument
  • Make name required for Meter
  • Add prefix NOOP_ to no-op constant instruments
  • Normalize Instrument names with downcase before registering them
    because their name should be case-insensitive
  • Add advice to Synchronous Instruments

Comment on lines +19 to +21

Naming/VariableNumber:
Enabled: false
Copy link
Contributor Author

@elias19r elias19r May 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to revert any of these style changes, just let me know

nit: For this one, I think that naming variables with something_1 and something_2 in tests is easier to read

Comment on lines +25 to +27

Style/IfUnlessModifier:
Enabled: false
Copy link
Contributor Author

@elias19r elias19r May 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: When this is enabled, we can be forced into writing long lines with if/unless at the end which I think may not be easy to read

Comment on lines -59 to -63
raise InstrumentNameError if name.nil?
raise InstrumentNameError if name.empty?
raise InstrumentNameError unless NAME_REGEX.match?(name)
raise InstrumentUnitError if unit && (!unit.ascii_only? || unit.size > 63)
raise InstrumentDescriptionError if description && (description.size > 1023 || !utf8mb3_encoding?(description.dup))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After reading Synchronous Instrument API I noticed the following

The API SHOULD be documented in a way to communicate to users that the
name parameter needs to conform to the instrument name syntax.
The API SHOULD NOT validate the name; that is left to
implementations of the API.

and

The unit parameter needs to support the instrument unit rule.
Meaning, the API MUST accept a case-sensitive string that supports
ASCII character encoding and can hold at least 63 characters.
The API SHOULD NOT validate the unit.

Similar for name and unit in Asynchronous Instrument API

So, I think we should remove these validations from here and add yard doc comments instead. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I think we should remove these validations from here and add yard doc comments instead. What do you think?

I think that's reasonable, the API spec is explicit in that it should perform any validations against the name field, so that will be the responsibility of the SDK implementation. Yard docs seem like a good solution for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also in the "Metrics No-Op API Implementation" docs it makes clear No-Op Meters must not validate. For example:

The Meter MUST accept these parameters. However, the Meter MUST NOT validate any argument it receives.

https://opentelemetry.io/docs/specs/otel/metrics/noop/#counter-creation

raise InstrumentNameError unless NAME_REGEX.match?(name)
raise InstrumentUnitError if unit && (!unit.ascii_only? || unit.size > 63)
raise InstrumentDescriptionError if description && (description.size > 1023 || !utf8mb3_encoding?(description.dup))
name = name.downcase
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is new. From the Metrics' Meter API specification for the Instrument Name Syntax it says:

They are case-insensitive, ASCII strings.

So here I'm normalizing the name with downcase before using it in @instrument_registry[name]

Comment on lines 22 to 40
# @param callback [Proc, Array<Proc>]
# Callback functions should:
# - be reentrant safe;
# - not take an indefinite amount of time;
# - not make duplicate observations (more than one Measurement with the same attributes)
# across all registered callbacks;
def register_callback(callback)
@callback.concat(Array(callback))
end

# @param callback [Proc, Array<Proc>]
# Callback functions should:
# - be reentrant safe;
# - not take an indefinite amount of time;
# - not make duplicate observations (more than one Measurement with the same attributes)
# across all registered callbacks;
def unregister_callback(callback)
@callback -= Array(callback)
end
Copy link
Contributor Author

@elias19r elias19r May 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how I imagine register/unregister callback functions would look like in Ruby. Feel free to suggest any changes

https://opentelemetry.io/docs/specs/otel/metrics/api/#asynchronous-instrument-api

Copy link
Contributor Author

@elias19r elias19r May 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized that, with this, people are able to register callbacks to No-Op instances of Asynchronous Instruments. I think I should delete these methods' bodies and leave them to be implemented by the SDK

Updated in b0ba487

@robbkidd robbkidd added this to the Metrics v1.0 milestone May 16, 2023
attr_reader :name, :unit, :description

# @api private
def initialize(name, unit: nil, description: nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • advice for implementations.

Users can provide advice, but its up to their discretion. Therefore, this API needs to be structured to accept advice, but MUST NOT obligate the user to provide it.

advice needs to be structured as described in instrument advice, with parameters that are general and specific to a particular instrument kind. The API SHOULD NOT validate advice.

Did you want to work this in as a follow up PR? https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#synchronous-instrument-api

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that advice has been added recently. I can include it in this PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 7cce415

I'm not sure how advice will be exactly used, but I think of it as an "options" Hash with recommended values. For example in docs, the recommended bucket boundaries for a histogram. I imagine they will add standard names to be used in advice

metrics_api/lib/opentelemetry/metrics/meter.rb Outdated Show resolved Hide resolved
Comment on lines -59 to -63
raise InstrumentNameError if name.nil?
raise InstrumentNameError if name.empty?
raise InstrumentNameError unless NAME_REGEX.match?(name)
raise InstrumentUnitError if unit && (!unit.ascii_only? || unit.size > 63)
raise InstrumentDescriptionError if description && (description.size > 1023 || !utf8mb3_encoding?(description.dup))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I think we should remove these validations from here and add yard doc comments instead. What do you think?

I think that's reasonable, the API spec is explicit in that it should perform any validations against the name field, so that will be the responsibility of the SDK implementation. Yard docs seem like a good solution for this.

@elias19r
Copy link
Contributor Author

If you squash&merge, feel free to use the PR's description in the commit message (and add/edit anything). Otherwise, let me know and I can re-create commits with better messages :D

@robertlaurin
Copy link
Contributor

@elias19r I'm about ready to merge, there's some branch conflicts that need to be resolved first though.

module OpenTelemetry
module Metrics
module Instrument
# https://opentelemetry.io/docs/reference/specification/metrics/api/#asynchronous-instrument-api
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This link didn't work for me. I found the doc at:

Suggested change
# https://opentelemetry.io/docs/reference/specification/metrics/api/#asynchronous-instrument-api
# https://opentelemetry.io/docs/specs/otel/metrics/api/#asynchronous-instrument-api

Copy link
Contributor Author

@elias19r elias19r Jun 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@name = name
@unit = unit || ''
@description = description || ''
@callback = callback ? Array(callback) : []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: since this is an array of functions, can we pluralize the name as callbacks?

Copy link
Contributor Author

@elias19r elias19r Jun 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I can pluralize them!

I think the reason I initially didn't do that was to keep the same "symbol same" from the specification although this @callback instance variable is internal.

But in Meter API, in metrics_api/lib/opentelemetry/metrics/meter.rb, should I pluralize the keyword arg callback: as well in the create_observable_counter, create_observable_gauge, and create_observable_up_down_counter methods?

Updated in 443d35e

# - not take an indefinite amount of time;
# - not make duplicate observations (more than one Measurement with the same attributes)
# across all registered callbacks;
def register_callback(callback); end
Copy link
Contributor

@fbogsany fbogsany Jun 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, the plural form is preferable. I'd also like to use the "splat" form:

Suggested change
def register_callback(callback); end
def register_callbacks(*callback); end

This allows calling as:

instr.register_callbacks(cb1)
instr.register_callbacks(cb2, cb3)

instead of

instr.register_callback(cb1)
instr.register_callback([cb2, cb3])

Copy link
Contributor Author

@elias19r elias19r Jun 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the splat form, but I've been thinking if should actually support all those cases later in the implementation:

instr.register_callback(cb1)
instr.register_callback(cb2, cb3)
instr.register_callback([cb4, cb5]) # And support this as well?
# Just so it is consistent with `callbacks:` from the `Meter#create_observable_counter` API

In the implementation, I think we could support that with

def register_callbacks(*callbacks)
  @callbacks.concat(callbacks.flatten)
end

Because otherwise it would feel odd to be able to pass an array of procs when creating an instrument, like

meter = MeterProvider.meter('test')
instr = meter.create_observable_counter('test', callbacks: [cb1, cb2])

but not be able to pass an array of procs when registering callbacks later

meter = MeterProvider.meter('test')
instr = meter.create_observable_counter('test')

instr.register_callbacks([cb1, cb2]) # ??

Let me know what you think! Updated in 9d9ad67


assert(instrument.register_callbacks(-> {}).nil?)
assert(instrument.register_callbacks(-> {}, -> {}).nil?)
assert(instrument.register_callbacks([-> {}, -> {}]).nil?)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert(instrument.register_callbacks([-> {}, -> {}]).nil?)
assert(instrument.register_callbacks(-> {}, -> {}).nil?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of allowing all these use cases:

instr.register_callback(cb1)
instr.register_callback(cb2, cb3)
instr.register_callback([cb4, cb5]) # And support this as well?
# Just so it is consistent with `callbacks:` from the `Meter#create_observable_counter` API

See #1442 (comment)


assert(instrument.unregister_callbacks(-> {}).nil?)
assert(instrument.unregister_callbacks(-> {}, -> {}).nil?)
assert(instrument.unregister_callbacks([-> {}, -> {}]).nil?)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert(instrument.unregister_callbacks([-> {}, -> {}]).nil?)
assert(instrument.unregister_callbacks(-> {}, -> {}).nil?)

@callbacks = callbacks ? Array(callbacks) : []
end

# @param callbacks [Proc, Array<Proc>]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, callbacks is an Array<Proc> now, always - I'm not sure how to document a "splat" param.

# across all registered callbacks;
def register_callbacks(*callbacks); end

# @param callbacks [Proc, Array<Proc>]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Comment on lines +83 to +84
# @param advice [optional Hash] Set of recommendations aimed at assisting
# implementations in providing useful output with minimal configuration
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a better description of advice? I'm working through the spec, and I haven't found anything useful yet. I have no idea how to use it - Hash is very generic - what are allowed key & value types?

I would have expected advice to be a simple String, with set here not meaning an actual data structure, but I feel like I'm missing a lot of context.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems advice has been added to the specification recently. I'm not sure how it will be exactly used. For now, I'm thinking of it as an "options" Hash with recommended values. For example in docs, the recommended bucket boundaries for a histogram. I imagine they will define standard names to be used in advice, maybe:

advice: {
  histogram: {
    explicit_bucket_boundaries: [0.1, 0.5, 1.0, 5.0, 10.0, 25.0, Float::INFINITY]
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading the SDK page, I found another reference to advice in Default Aggregation:

The Default Aggregation informs the SDK to use the Instrument kind to select an aggregation and advice to influence aggregation configuration parameters (as noted in the "Selected Aggregation" column).

Instrument Kind Selected Aggregation
[...] [...]
Histogram Explicit Bucket Histogram Aggregation, with ExplicitBucketBoundaries from advice if provided

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should block the current PR from being merged, but it seems that advice has been renamed to advisory parameters.

It has a mixed status in the API and is experimental in the SDK.

@elias19r
Copy link
Contributor Author

@robertlaurin @fbogsany Hi, I haven't had a lot of time to work on this, but I could start a follow-up PR adjusting the existing SDK code to the changes I'm making to the API here.

thoughtbot#1
(I've opened it in another repo just for visibility. I'll move to here if this PR gets merged)

It is mainly about making the Proxy Instruments and SDK Instruments inherit from the API Instrument classes. If you get a chance to review it, let me know if that makes sense :)

After that, I wonder if we could complete the implementation of one instrument (maybe Counter). I don't quite see the big picture yet, but I'm starting to understand the concepts of the data model for metrics.

@Hronom
Copy link

Hronom commented Sep 17, 2023

We interested in getting suport for metrics at least no auto instrumentation for ruby, it's shame that it still not have is maintainers can implement it?

@elias19r
Copy link
Contributor Author

elias19r commented Dec 8, 2023

I think I won't be able to keep up with those PRs, it's been a while since I read the docs. I'll close them but keep the branches in case the changes are useful.

@elias19r elias19r closed this Dec 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants