From 535aa72ba3643384c53dfcbb272e5c87496a670d Mon Sep 17 00:00:00 2001 From: Bart de Water <118401830+bdewater-thatch@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:00:26 -0400 Subject: [PATCH] feat(semconv): autoload namespaces so a single require exposes all constants Consuming apps previously had to require each SemConv namespace by hand (`require 'opentelemetry/semconv/http'`, ...), which got verbose. Register every stable and incubating namespace with Ruby's `autoload` instead, so a single `require 'opentelemetry-semantic_conventions'` makes them all referenceable while still loading a namespace's file only on first use -- preserving the goal of not pulling in constants an app never touches. - Generate lib/opentelemetry/semconv.rb (autoload manifest) via a new `generate_autoload_manifest` Rake task wired into `rake generate`; it reads the existing rollup files, so it needs no Docker/weaver. - Require the manifest from the gem entrypoint. Backward compatible: manual per-namespace requires and the legacy SemanticConventions path still work. - Document the single-require pattern in the README. - Add tests covering lazy loading (in a clean subprocess) and a drift guard asserting every generated rollup has an autoload entry. --- semantic_conventions/README.md | 15 +- semantic_conventions/Rakefile | 44 +++++- .../lib/opentelemetry-semantic_conventions.rb | 1 + .../lib/opentelemetry/semconv.rb | 139 ++++++++++++++++++ .../opentelemetry/semconv/autoload_test.rb | 46 ++++++ 5 files changed, 240 insertions(+), 5 deletions(-) create mode 100644 semantic_conventions/lib/opentelemetry/semconv.rb create mode 100644 semantic_conventions/test/opentelemetry/semconv/autoload_test.rb diff --git a/semantic_conventions/README.md b/semantic_conventions/README.md index 98d299484c..fade506b76 100644 --- a/semantic_conventions/README.md +++ b/semantic_conventions/README.md @@ -55,12 +55,19 @@ These constants will be preserved to avoid breaking changes for users who rely on the old constants. These constants do not differentiate between stable and unstable constants. New constants will not be added to this namespace. -We recommend you require only the files that contain the constants you are going -to use. For example, if you were creating instrumentation for an HTTP Client -that emits only stable conventions, you would likely require: +Require the gem once and reference any constant directly. Each namespace is +registered with Ruby's `autoload`, so the file backing a namespace is only loaded +the first time you reference one of its constants — you don't pay to load +conventions you never use. ```rb -require 'opentelemetry/semconv/http' +require 'opentelemetry-semantic_conventions' + +# Stable +OpenTelemetry::SemConv::HTTP::HTTP_REQUEST_METHOD # => 'http.request.method' + +# Incubating (experimental/deprecated) +OpenTelemetry::SemConv::Incubating::GEN_AI::GEN_AI_SYSTEM ``` If you want to require all of the 1.11.0 constants, you can use: diff --git a/semantic_conventions/Rakefile b/semantic_conventions/Rakefile index 495f9e28b1..d4c9217c01 100644 --- a/semantic_conventions/Rakefile +++ b/semantic_conventions/Rakefile @@ -45,7 +45,7 @@ default_tasks = task default: default_tasks desc 'update semantic conventions' -task generate: %i[update_spec_version_constant generate_require_rollups] +task generate: %i[update_spec_version_constant generate_require_rollups generate_autoload_manifest] SPEC_VERSION = '1.41.1' OTEL_WEAVER_VERSION = 'v0.22.1' @@ -122,6 +122,48 @@ task generate_require_rollups: %i[generate_semconv rubocop_autocorrect] do end end +# Render the autoload declarations for the given rollup files, indented for the +# manifest. The module constant is the screaming_snake_case of the namespace, +# which for these snake_case file names is just the upcased basename +# (e.g. gen_ai.rb => GEN_AI). +def semconv_autoloads(rollups, require_prefix, indent) + rollups.sort.map do |rollup| + namespace = rollup.basename('.rb').to_s + "#{' ' * indent}autoload :#{namespace.upcase}, '#{require_prefix}/#{namespace}'" + end.join("\n") +end + +desc 'Generate the autoload manifest so a single require lazily exposes every namespace.' +task :generate_autoload_manifest do + puts "\n+++ Generating autoload manifest for the SemConv namespace.\n" + + # The per-namespace rollup files sit directly under semconv_output_dir (stable) + # and under its incubating/ subdirectory. + stable = semconv_autoloads(semconv_output_dir.glob('*.rb'), 'opentelemetry/semconv', 4) + incubating = semconv_autoloads(semconv_output_dir.join('incubating').glob('*.rb'), 'opentelemetry/semconv/incubating', 6) + + manifest_filename = semconv_output_dir.dirname + "#{semconv_output_dir.basename}.rb" + File.write(manifest_filename, <<~MANIFEST) + # frozen_string_literal: true + + #{COPYRIGHT_NOTICE} + # This file was autogenerated. Do not edit it by hand. + + module OpenTelemetry + # Stable OpenTelemetry semantic convention constants, autoloaded per namespace. + module SemConv + #{stable} + + # Experimental, development, or deprecated semantic convention constants. + module Incubating + #{incubating} + end + end + end + MANIFEST + puts "✅ Generated file \"#{manifest_filename}\"" +end + desc 'Bump the semantic_conventions gem version to match the spec' task :update_spec_version_constant do puts "\n+++ Updating library SPEC_VERSION constant to #{SPEC_VERSION}.\n" diff --git a/semantic_conventions/lib/opentelemetry-semantic_conventions.rb b/semantic_conventions/lib/opentelemetry-semantic_conventions.rb index d2f4552f9d..36ab6f73ad 100644 --- a/semantic_conventions/lib/opentelemetry-semantic_conventions.rb +++ b/semantic_conventions/lib/opentelemetry-semantic_conventions.rb @@ -5,3 +5,4 @@ # SPDX-License-Identifier: Apache-2.0 require_relative 'opentelemetry/semantic_conventions' +require_relative 'opentelemetry/semconv' diff --git a/semantic_conventions/lib/opentelemetry/semconv.rb b/semantic_conventions/lib/opentelemetry/semconv.rb new file mode 100644 index 0000000000..7dae0f1109 --- /dev/null +++ b/semantic_conventions/lib/opentelemetry/semconv.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +# This file was autogenerated. Do not edit it by hand. + +module OpenTelemetry + # Stable OpenTelemetry semantic convention constants, autoloaded per namespace. + module SemConv + autoload :ASPNETCORE, 'opentelemetry/semconv/aspnetcore' + autoload :CLIENT, 'opentelemetry/semconv/client' + autoload :CODE, 'opentelemetry/semconv/code' + autoload :DB, 'opentelemetry/semconv/db' + autoload :DEPLOYMENT, 'opentelemetry/semconv/deployment' + autoload :DOTNET, 'opentelemetry/semconv/dotnet' + autoload :ERROR, 'opentelemetry/semconv/error' + autoload :EXCEPTION, 'opentelemetry/semconv/exception' + autoload :HTTP, 'opentelemetry/semconv/http' + autoload :JVM, 'opentelemetry/semconv/jvm' + autoload :KESTREL, 'opentelemetry/semconv/kestrel' + autoload :NETWORK, 'opentelemetry/semconv/network' + autoload :OTEL, 'opentelemetry/semconv/otel' + autoload :SERVER, 'opentelemetry/semconv/server' + autoload :SERVICE, 'opentelemetry/semconv/service' + autoload :SIGNALR, 'opentelemetry/semconv/signalr' + autoload :TELEMETRY, 'opentelemetry/semconv/telemetry' + autoload :URL, 'opentelemetry/semconv/url' + autoload :USER_AGENT, 'opentelemetry/semconv/user_agent' + + # Experimental, development, or deprecated semantic convention constants. + module Incubating + autoload :ANDROID, 'opentelemetry/semconv/incubating/android' + autoload :APP, 'opentelemetry/semconv/incubating/app' + autoload :ARTIFACT, 'opentelemetry/semconv/incubating/artifact' + autoload :ASPNETCORE, 'opentelemetry/semconv/incubating/aspnetcore' + autoload :AWS, 'opentelemetry/semconv/incubating/aws' + autoload :AZ, 'opentelemetry/semconv/incubating/az' + autoload :AZURE, 'opentelemetry/semconv/incubating/azure' + autoload :BROWSER, 'opentelemetry/semconv/incubating/browser' + autoload :CASSANDRA, 'opentelemetry/semconv/incubating/cassandra' + autoload :CICD, 'opentelemetry/semconv/incubating/cicd' + autoload :CLIENT, 'opentelemetry/semconv/incubating/client' + autoload :CLOUD, 'opentelemetry/semconv/incubating/cloud' + autoload :CLOUDEVENTS, 'opentelemetry/semconv/incubating/cloudevents' + autoload :CLOUDFOUNDRY, 'opentelemetry/semconv/incubating/cloudfoundry' + autoload :CODE, 'opentelemetry/semconv/incubating/code' + autoload :CONTAINER, 'opentelemetry/semconv/incubating/container' + autoload :CPU, 'opentelemetry/semconv/incubating/cpu' + autoload :CPYTHON, 'opentelemetry/semconv/incubating/cpython' + autoload :DB, 'opentelemetry/semconv/incubating/db' + autoload :DEPLOYMENT, 'opentelemetry/semconv/incubating/deployment' + autoload :DESTINATION, 'opentelemetry/semconv/incubating/destination' + autoload :DEVICE, 'opentelemetry/semconv/incubating/device' + autoload :DISK, 'opentelemetry/semconv/incubating/disk' + autoload :DNS, 'opentelemetry/semconv/incubating/dns' + autoload :DOTNET, 'opentelemetry/semconv/incubating/dotnet' + autoload :ELASTICSEARCH, 'opentelemetry/semconv/incubating/elasticsearch' + autoload :ENDUSER, 'opentelemetry/semconv/incubating/enduser' + autoload :ERROR, 'opentelemetry/semconv/incubating/error' + autoload :EVENT, 'opentelemetry/semconv/incubating/event' + autoload :EXCEPTION, 'opentelemetry/semconv/incubating/exception' + autoload :FAAS, 'opentelemetry/semconv/incubating/faas' + autoload :FEATURE_FLAG, 'opentelemetry/semconv/incubating/feature_flag' + autoload :FILE, 'opentelemetry/semconv/incubating/file' + autoload :GCP, 'opentelemetry/semconv/incubating/gcp' + autoload :GEN_AI, 'opentelemetry/semconv/incubating/gen_ai' + autoload :GEO, 'opentelemetry/semconv/incubating/geo' + autoload :GO, 'opentelemetry/semconv/incubating/go' + autoload :GRAPHQL, 'opentelemetry/semconv/incubating/graphql' + autoload :HEROKU, 'opentelemetry/semconv/incubating/heroku' + autoload :HOST, 'opentelemetry/semconv/incubating/host' + autoload :HTTP, 'opentelemetry/semconv/incubating/http' + autoload :HW, 'opentelemetry/semconv/incubating/hw' + autoload :IOS, 'opentelemetry/semconv/incubating/ios' + autoload :JSONRPC, 'opentelemetry/semconv/incubating/jsonrpc' + autoload :JVM, 'opentelemetry/semconv/incubating/jvm' + autoload :K8S, 'opentelemetry/semconv/incubating/k8s' + autoload :KESTREL, 'opentelemetry/semconv/incubating/kestrel' + autoload :LINUX, 'opentelemetry/semconv/incubating/linux' + autoload :LOG, 'opentelemetry/semconv/incubating/log' + autoload :MAINFRAME, 'opentelemetry/semconv/incubating/mainframe' + autoload :MCP, 'opentelemetry/semconv/incubating/mcp' + autoload :MESSAGE, 'opentelemetry/semconv/incubating/message' + autoload :MESSAGING, 'opentelemetry/semconv/incubating/messaging' + autoload :NET, 'opentelemetry/semconv/incubating/net' + autoload :NETWORK, 'opentelemetry/semconv/incubating/network' + autoload :NFS, 'opentelemetry/semconv/incubating/nfs' + autoload :NODEJS, 'opentelemetry/semconv/incubating/nodejs' + autoload :OCI, 'opentelemetry/semconv/incubating/oci' + autoload :ONC_RPC, 'opentelemetry/semconv/incubating/onc_rpc' + autoload :OPENAI, 'opentelemetry/semconv/incubating/openai' + autoload :OPENSHIFT, 'opentelemetry/semconv/incubating/openshift' + autoload :OPENTRACING, 'opentelemetry/semconv/incubating/opentracing' + autoload :ORACLE, 'opentelemetry/semconv/incubating/oracle' + autoload :ORACLE_CLOUD, 'opentelemetry/semconv/incubating/oracle_cloud' + autoload :OS, 'opentelemetry/semconv/incubating/os' + autoload :OTEL, 'opentelemetry/semconv/incubating/otel' + autoload :OTHER, 'opentelemetry/semconv/incubating/other' + autoload :PEER, 'opentelemetry/semconv/incubating/peer' + autoload :POOL, 'opentelemetry/semconv/incubating/pool' + autoload :PPROF, 'opentelemetry/semconv/incubating/pprof' + autoload :PROCESS, 'opentelemetry/semconv/incubating/process' + autoload :PROFILE, 'opentelemetry/semconv/incubating/profile' + autoload :RPC, 'opentelemetry/semconv/incubating/rpc' + autoload :SECURITY_RULE, 'opentelemetry/semconv/incubating/security_rule' + autoload :SERVER, 'opentelemetry/semconv/incubating/server' + autoload :SERVICE, 'opentelemetry/semconv/incubating/service' + autoload :SESSION, 'opentelemetry/semconv/incubating/session' + autoload :SIGNALR, 'opentelemetry/semconv/incubating/signalr' + autoload :SOURCE, 'opentelemetry/semconv/incubating/source' + autoload :SYSTEM, 'opentelemetry/semconv/incubating/system' + autoload :TELEMETRY, 'opentelemetry/semconv/incubating/telemetry' + autoload :TEST, 'opentelemetry/semconv/incubating/test' + autoload :THREAD, 'opentelemetry/semconv/incubating/thread' + autoload :TLS, 'opentelemetry/semconv/incubating/tls' + autoload :URL, 'opentelemetry/semconv/incubating/url' + autoload :USER, 'opentelemetry/semconv/incubating/user' + autoload :USER_AGENT, 'opentelemetry/semconv/incubating/user_agent' + autoload :V8JS, 'opentelemetry/semconv/incubating/v8js' + autoload :VCS, 'opentelemetry/semconv/incubating/vcs' + autoload :WEBENGINE, 'opentelemetry/semconv/incubating/webengine' + autoload :ZOS, 'opentelemetry/semconv/incubating/zos' + end + end +end diff --git a/semantic_conventions/test/opentelemetry/semconv/autoload_test.rb b/semantic_conventions/test/opentelemetry/semconv/autoload_test.rb new file mode 100644 index 0000000000..2910c2fb9a --- /dev/null +++ b/semantic_conventions/test/opentelemetry/semconv/autoload_test.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +# test_helper eager-requires every file, which would mask autoload, so the +# lazy-loading behaviour is exercised in a clean subprocess. +describe 'OpenTelemetry::SemConv autoload' do + let(:gem_root) { File.expand_path('../../..', __dir__) } + + it 'lazily exposes stable and incubating namespaces from a single require' do + script = <<~RUBY + require 'opentelemetry-semantic_conventions' + raise 'stable namespace not autoloaded' unless OpenTelemetry::SemConv::HTTP::HTTP_REQUEST_METHOD == 'http.request.method' + raise 'incubating namespace not autoloaded' unless OpenTelemetry::SemConv::Incubating::GEN_AI::GEN_AI_SYSTEM == 'gen_ai.system' + RUBY + + lib = File.join(gem_root, 'lib') + api_lib = File.expand_path('../api/lib', gem_root) + ok = system(Gem.ruby, "-I#{lib}", "-I#{api_lib}", '-e', script) + + assert ok, 'a single require should lazily load both stable and incubating constants' + end + + it 'registers an autoload for every generated namespace rollup' do + manifest = File.read(File.join(gem_root, 'lib', 'opentelemetry', 'semconv.rb')) + + namespaces_in('*.rb').each do |ns| + assert_includes manifest, "autoload :#{ns.upcase}, 'opentelemetry/semconv/#{ns}'" + end + + namespaces_in('incubating', '*.rb').each do |ns| + assert_includes manifest, "autoload :#{ns.upcase}, 'opentelemetry/semconv/incubating/#{ns}'" + end + end + + private + + # Namespace basenames of the rollup files matched by +glob+ under the semconv dir. + def namespaces_in(*glob) + Dir[File.join(gem_root, 'lib', 'opentelemetry', 'semconv', *glob)].map { |f| File.basename(f, '.rb') } + end +end