cinc-otel 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ce4eaaf69cd3aac06552a6caee779015421915aa96bf2485295898d7488e0354
4
+ data.tar.gz: 968d002ac34cc401777b1b2ef073530675720c21cedf0a4fef6d4121f4833f50
5
+ SHA512:
6
+ metadata.gz: 7616575341ee0cb0a51aef37e2c65212a7cf6c8b4eecf600c87594308ca68d16941273aae9dab3a05daf9b312daa9b8026fbb9aab97a476ae7d25504be1ff0d3
7
+ data.tar.gz: ce767e8287d05498f9264ba39a4993d0845c3c802ea9723309b69289b3bb720ed495e0b357e093e6922b69de644ac95f3b09a306e897cb7e1f02d4061d3fc9e3
data/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [1.0.0]
11
+
12
+ ### Added
13
+
14
+ - `CincOtel::Handler`, a `Chef::EventDispatch` subscriber that emits an
15
+ OpenTelemetry span tree for each Cinc Client run: a `chef.run` root span, a
16
+ span per run phase, and a span per resource action.
17
+ - `__tenant` resource attribute tagging via `OTEL_RESOURCE_ATTRIBUTES` or the
18
+ `register!(tenant:)` keyword.
19
+ - Loads the OTLP exporter at require time so the default
20
+ `OTEL_TRACES_EXPORTER=otlp` configuration exports spans without extra setup.
21
+ - Nests the run under a parent trace supplied via the W3C `TRACEPARENT` /
22
+ `TRACESTATE` environment variables, so a run joins an OpenTelemetry-aware
23
+ caller's trace.
24
+
25
+ [Unreleased]: https://github.com/fastly/cinc-otel/compare/v1.0.0...HEAD
26
+ [1.0.0]: https://github.com/fastly/cinc-otel/releases/tag/v1.0.0
data/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright (c) 2026 Jason Cook
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,176 @@
1
+ # cinc-otel
2
+
3
+ OpenTelemetry run tracing for [Cinc Client](https://cinc.sh/).
4
+
5
+ `cinc-otel` emits an OpenTelemetry trace for each Cinc Client run: a `chef.run`
6
+ root span, a child span for each run phase (node load, cookbook sync,
7
+ compilation, converge, ...), and one span per resource action. The trace makes
8
+ it easy to see where run time is spent and to correlate failed or slow runs
9
+ with the rest of your distributed tracing in a backend such as Tempo or Jaeger.
10
+
11
+ It is implemented as a `Chef::EventDispatch` handler. The event stream is the
12
+ only interface that delivers live, paired start/complete events for every phase
13
+ and resource action, which map one-to-one onto span open/close with real
14
+ wall-clock timestamps.
15
+
16
+ ## Requirements
17
+
18
+ `cinc-otel` is loaded **inside a cinc-client process** and relies on Chef's
19
+ `Chef::EventDispatch::Base`, `Chef::Log`, and `Chef::Config` already being
20
+ loaded. It does not depend on the `chef` gem itself and does not pull it in;
21
+ Chef is expected to be present in the runtime that loads this gem. The gem is
22
+ tested against Cinc Client 18 and 19.
23
+
24
+ The OpenTelemetry SDK and OTLP exporter (`opentelemetry-sdk`,
25
+ `opentelemetry-exporter-otlp`) are runtime dependencies and are required when
26
+ the handler loads, so they are always available.
27
+
28
+ ## Installation
29
+
30
+ Add it to the gems available to cinc-client. For example, in a Cinc
31
+ installation that uses `cinc gem install`:
32
+
33
+ ```shell
34
+ cinc gem install cinc-otel
35
+ ```
36
+
37
+ Or add it to your bundle:
38
+
39
+ ```ruby
40
+ gem "cinc-otel"
41
+ ```
42
+
43
+ ## Enabling tracing
44
+
45
+ Add the following to `client.rb` (or `solo.rb`):
46
+
47
+ ```ruby
48
+ require "cinc-otel"
49
+ CincOtel::Handler.register!
50
+ ```
51
+
52
+ `register!` is idempotent: applications such as chef-solo evaluate the config
53
+ file more than once, and only one handler is ever registered.
54
+
55
+ The exporter and endpoint are configured through the standard `OTEL_*`
56
+ environment variables, for example:
57
+
58
+ ```shell
59
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
60
+ ```
61
+
62
+ If span emission ever raises mid-run, the handler disables itself for the rest
63
+ of the run rather than letting the error propagate — tracing can never break a
64
+ converge.
65
+
66
+ If something else in the process has already configured the global
67
+ OpenTelemetry SDK before the handler loads, that configuration (including its
68
+ resource attributes) is used as-is.
69
+
70
+ ## Service name and tenant
71
+
72
+ The service name defaults to `chef-client` and can be overridden with
73
+ `OTEL_SERVICE_NAME`.
74
+
75
+ Spans can be tagged with a `__tenant` resource attribute (used by Tempo
76
+ multi-tenancy for search and tail-sampling routing) either through the standard
77
+ env var:
78
+
79
+ ```shell
80
+ OTEL_RESOURCE_ATTRIBUTES=__tenant=controlplane
81
+ ```
82
+
83
+ or explicitly at registration, which takes precedence over the env var:
84
+
85
+ ```ruby
86
+ CincOtel::Handler.register!(tenant: "controlplane")
87
+ ```
88
+
89
+ ## Connecting to a parent trace
90
+
91
+ When cinc-client is invoked by an OpenTelemetry-aware caller (a CI runner,
92
+ `otel-cli`, an orchestrator), export the standard W3C Trace Context
93
+ environment variables before the run and the `chef.run` span — and the whole
94
+ run tree beneath it — will nest under the caller's span instead of starting a
95
+ new trace:
96
+
97
+ ```shell
98
+ export TRACEPARENT=00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
99
+ export TRACESTATE=vendor=value # optional
100
+ cinc-client
101
+ ```
102
+
103
+ `TRACEPARENT`/`TRACESTATE` are the W3C Trace Context format emitted by tools
104
+ such as `otel-cli`, GitHub Actions, and Buildkite. If `TRACEPARENT` is absent
105
+ or malformed, the run starts its own root trace as usual.
106
+
107
+ ## Span structure
108
+
109
+ ```text
110
+ chef.run chef.version, chef.run_id, chef.node.name, chef.environment
111
+ ├─ chef.registration
112
+ ├─ chef.node_load
113
+ ├─ chef.cookbook_resolution
114
+ ├─ chef.cookbook_clean
115
+ ├─ chef.cookbook_sync
116
+ ├─ chef.cookbook_gems
117
+ ├─ chef.cookbook_compilation
118
+ │ ├─ chef.library_load
119
+ │ ├─ chef.ohai_plugin_load
120
+ │ ├─ chef.compliance_load
121
+ │ ├─ chef.attribute_load
122
+ │ ├─ chef.lwrp_load
123
+ │ ├─ chef.definition_load
124
+ │ └─ chef.recipe_load
125
+ ├─ chef.converge
126
+ │ └─ one span per resource action, named e.g. file[/etc/motd]
127
+ └─ chef.handlers
128
+ ```
129
+
130
+ Each resource span carries `chef.resource.type`, `chef.resource.name`,
131
+ `chef.resource.action`, `chef.resource.cookbook` and `chef.resource.recipe`,
132
+ plus outcome detail:
133
+
134
+ * `chef.resource.updated` — `true`/`false` for converged resources
135
+ * `chef.resource.skipped` and `chef.resource.skip_reason` — for resources
136
+ skipped by a guard or `action :nothing`
137
+ * `chef.resource.notification_type` and `chef.resource.notifying_resource` —
138
+ when the action ran due to a notification
139
+ * failed resources record the exception on the span and set error status;
140
+ retries appear as `retry` span events
141
+
142
+ ## Testing locally
143
+
144
+ The SDK's console exporter prints spans to stdout, which is handy for verifying
145
+ output without a collector:
146
+
147
+ ```shell
148
+ OTEL_TRACES_EXPORTER=console cinc-solo -c solo.rb -o 'recipe[my_cookbook]'
149
+ ```
150
+
151
+ ## Development
152
+
153
+ ```shell
154
+ bundle install
155
+ bundle exec rake # runs chefstyle + rspec
156
+ bundle exec rspec # tests only
157
+ bundle exec rake style # lint only
158
+ ```
159
+
160
+ The unit suite runs without a Chef installation beyond the `chef` dev gem. There
161
+ is also an end-to-end integration test that runs a real `chef-solo`/`cinc-solo`
162
+ converge and asserts the emitted OpenTelemetry spans over the OTLP exporter. It
163
+ is excluded from the default run and is skipped unless a solo binary is present:
164
+
165
+ ```shell
166
+ CINC_OTEL_INTEGRATION=1 bundle exec rspec --tag integration spec/integration
167
+ # or
168
+ bundle exec rake integration
169
+ ```
170
+
171
+ CI runs it in a dedicated job that installs Cinc Client and installs the built
172
+ gem into its embedded Ruby.
173
+
174
+ ## License
175
+
176
+ Apache-2.0. See [LICENSE](LICENSE).
data/lib/cinc-otel.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "cinc_otel"
@@ -0,0 +1,281 @@
1
+ #
2
+ # Author:: Jason Cook (<jasonc@simpleideas.org>)
3
+ # Copyright:: Copyright (c) Jason Cook
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require "opentelemetry/sdk"
20
+ # Loaded so the standard OTEL_TRACES_EXPORTER=otlp configuration works out of
21
+ # the box: the SDK only wires up an exporter whose constant is already loaded,
22
+ # it does not require the exporter gem itself.
23
+ require "opentelemetry-exporter-otlp"
24
+
25
+ require "chef/event_dispatch/base"
26
+ require "chef/log"
27
+ require "chef/config"
28
+ require "chef/version"
29
+
30
+ require_relative "version"
31
+
32
+ module CincOtel
33
+ # Emits an OpenTelemetry trace for the Cinc Client run: a root span for
34
+ # the run, child spans for each phase (node load, cookbook sync,
35
+ # compilation, converge, ...) and one span per resource action.
36
+ #
37
+ # Enable it from client.rb (or solo.rb):
38
+ #
39
+ # require "cinc-otel"
40
+ # CincOtel::Handler.register!
41
+ #
42
+ # Exporter and endpoint are configured through the standard OTEL_*
43
+ # environment variables. The opentelemetry-sdk gem (and
44
+ # opentelemetry-exporter-otlp for the default OTLP exporter) are runtime
45
+ # dependencies, so they are always available.
46
+ #
47
+ # Spans can be tagged with a __tenant resource attribute (used by Tempo
48
+ # multi-tenancy for search and tail-sampling routing) either through the
49
+ # standard env var:
50
+ #
51
+ # OTEL_RESOURCE_ATTRIBUTES=__tenant=controlplane
52
+ #
53
+ # or explicitly, which takes precedence over the env var:
54
+ #
55
+ # CincOtel::Handler.register!(tenant: "controlplane")
56
+ class Handler < Chef::EventDispatch::Base
57
+ # Adds the handler to Chef::Config[:event_handlers]. Idempotent, because
58
+ # some applications (chef-solo) evaluate the config file more than once
59
+ # and a second handler would emit every span twice.
60
+ def self.register!(**opts)
61
+ return if Chef::Config[:event_handlers].any? { |handler| handler.is_a?(self) }
62
+
63
+ Chef::Config[:event_handlers] << new(**opts)
64
+ end
65
+
66
+ def initialize(tracer_provider: nil, tenant: nil)
67
+ @stack = []
68
+ @tracer_provider = tracer_provider || default_tracer_provider(tenant)
69
+ @tracer = @tracer_provider&.tracer("cinc-otel", CincOtel::VERSION)
70
+ end
71
+
72
+ def run_start(version, run_status)
73
+ start_span("chef.run", {
74
+ "chef.version" => version.to_s,
75
+ "chef.run_id" => run_status.run_id.to_s,
76
+ }, with_parent: remote_parent_context)
77
+ end
78
+
79
+ def run_completed(node, run_status)
80
+ end_span("chef.run", ok: true)
81
+ @tracer_provider&.force_flush
82
+ end
83
+
84
+ def run_failed(exception, run_status)
85
+ end_span("chef.run", error: exception)
86
+ @tracer_provider&.force_flush
87
+ end
88
+
89
+ def node_load_success(node)
90
+ root_span&.set_attribute("chef.node.name", node.name.to_s)
91
+ root_span&.set_attribute("chef.environment", node.chef_environment.to_s)
92
+ end
93
+
94
+ def ohai_completed(node)
95
+ current_span&.add_event("ohai_completed")
96
+ end
97
+
98
+ def deprecation(message, location = nil)
99
+ text = message.respond_to?(:message) ? message.message.to_s : message.to_s
100
+ attributes = { "chef.deprecation.message" => text }
101
+ attributes["chef.deprecation.location"] = location.to_s if location
102
+ current_span&.add_event("deprecation", attributes:)
103
+ end
104
+
105
+ # Paired phase events: the start event opens a span, the end event
106
+ # closes it. Event arguments are not recorded on phase spans.
107
+ {
108
+ "chef.registration" => %i{registration_start registration_completed},
109
+ "chef.node_load" => %i{node_load_start node_load_completed},
110
+ "chef.cookbook_resolution" => %i{cookbook_resolution_start cookbook_resolution_complete},
111
+ "chef.cookbook_clean" => %i{cookbook_clean_start cookbook_clean_complete},
112
+ "chef.cookbook_sync" => %i{cookbook_sync_start cookbook_sync_complete},
113
+ "chef.cookbook_gems" => %i{cookbook_gem_start cookbook_gem_finished},
114
+ "chef.cookbook_compilation" => %i{cookbook_compilation_start cookbook_compilation_complete},
115
+ "chef.library_load" => %i{library_load_start library_load_complete},
116
+ "chef.lwrp_load" => %i{lwrp_load_start lwrp_load_complete},
117
+ "chef.ohai_plugin_load" => %i{ohai_plugin_load_start ohai_plugin_load_complete},
118
+ "chef.attribute_load" => %i{attribute_load_start attribute_load_complete},
119
+ "chef.definition_load" => %i{definition_load_start definition_load_complete},
120
+ "chef.recipe_load" => %i{recipe_load_start recipe_load_complete},
121
+ "chef.compliance_load" => %i{compliance_load_start compliance_load_complete},
122
+ "chef.converge" => %i{converge_start converge_complete},
123
+ "chef.handlers" => %i{handlers_start handlers_completed},
124
+ }.each do |span_name, (start_event, end_event)|
125
+ define_method(start_event) { |*_args| start_span(span_name) }
126
+ define_method(end_event) { |*_args| end_span(span_name) }
127
+ end
128
+
129
+ # Failure events close their phase span with error status; the value is
130
+ # the position of the exception in the event's arguments.
131
+ {
132
+ registration_failed: ["chef.registration", 1],
133
+ node_load_failed: ["chef.node_load", 1],
134
+ run_list_expand_failed: ["chef.node_load", 1],
135
+ cookbook_resolution_failed: ["chef.cookbook_resolution", 1],
136
+ cookbook_sync_failed: ["chef.cookbook_sync", 1],
137
+ cookbook_gem_failed: ["chef.cookbook_gems", 0],
138
+ converge_failed: ["chef.converge", 0],
139
+ }.each do |event, (span_name, exception_index)|
140
+ define_method(event) { |*args| end_span(span_name, error: args[exception_index]) }
141
+ end
142
+
143
+ def resource_action_start(resource, action, notification_type = nil, notifier = nil)
144
+ attributes = {
145
+ "chef.resource.type" => resource.resource_name.to_s,
146
+ "chef.resource.name" => resource.name.to_s,
147
+ "chef.resource.action" => action.to_s,
148
+ }
149
+ attributes["chef.resource.cookbook"] = resource.cookbook_name.to_s if resource.cookbook_name
150
+ attributes["chef.resource.recipe"] = resource.recipe_name.to_s if resource.recipe_name
151
+ attributes["chef.resource.notification_type"] = notification_type.to_s if notification_type
152
+ attributes["chef.resource.notifying_resource"] = notifier.to_s if notifier
153
+ start_span(resource.to_s, attributes)
154
+ end
155
+
156
+ def resource_updated(resource, action)
157
+ current_span&.set_attribute("chef.resource.updated", true)
158
+ end
159
+
160
+ def resource_up_to_date(resource, action)
161
+ current_span&.set_attribute("chef.resource.updated", false)
162
+ end
163
+
164
+ def resource_skipped(resource, action, conditional)
165
+ span = current_span
166
+ return unless span
167
+
168
+ span.set_attribute("chef.resource.skipped", true)
169
+ span.set_attribute("chef.resource.skip_reason", conditional.to_text)
170
+ end
171
+
172
+ def resource_failed(resource, action, exception)
173
+ span = current_span
174
+ return unless span
175
+
176
+ span.record_exception(exception)
177
+ span.status = OpenTelemetry::Trace::Status.error(exception.message)
178
+ end
179
+
180
+ def resource_failed_retriable(resource, action, retry_count, exception)
181
+ current_span&.add_event("retry", attributes: {
182
+ "chef.resource.retries_remaining" => retry_count,
183
+ "exception.message" => exception.message,
184
+ })
185
+ end
186
+
187
+ def resource_completed(resource)
188
+ end_span(resource.to_s)
189
+ end
190
+
191
+ private
192
+
193
+ def current_span
194
+ @stack.last && @stack.last[1]
195
+ end
196
+
197
+ def root_span
198
+ @stack.first && @stack.first[1]
199
+ end
200
+
201
+ # Opens a span as a child of whatever span is currently innermost and
202
+ # pushes it on the stack. Events arrive synchronously on one thread, so
203
+ # the stack mirrors the nesting of the run.
204
+ def start_span(name, attributes = {}, with_parent: nil)
205
+ return unless @tracer
206
+
207
+ parent = @stack.last
208
+ context = if parent
209
+ OpenTelemetry::Trace.context_with_span(parent[1])
210
+ else
211
+ with_parent || OpenTelemetry::Context.current
212
+ end
213
+ @stack << [name, @tracer.start_span(name, with_parent: context, attributes:)]
214
+ end
215
+
216
+ # Finishes the named span. If intervening spans were left open (an end
217
+ # event that never fired), they are finished too; if the named span is
218
+ # not open at all, this is a no-op.
219
+ def end_span(name, error: nil, ok: false)
220
+ return unless @stack.any? { |n, _| n == name }
221
+
222
+ loop do
223
+ span_name, span = @stack.pop
224
+ if span_name == name
225
+ if error
226
+ span.record_exception(error)
227
+ span.status = OpenTelemetry::Trace::Status.error(error.message)
228
+ elsif ok
229
+ span.status = OpenTelemetry::Trace::Status.ok
230
+ end
231
+ end
232
+ span.finish
233
+ break if span_name == name
234
+ end
235
+ end
236
+
237
+ # Extracts a remote trace context from the standard W3C TRACEPARENT (and
238
+ # optional TRACESTATE) environment variables so the run can nest under an
239
+ # OpenTelemetry-aware caller's trace. Absent or malformed headers yield the
240
+ # current (empty) context, so chef.run simply starts a new root trace.
241
+ def remote_parent_context
242
+ carrier = {}
243
+ carrier["traceparent"] = ENV["TRACEPARENT"] if ENV["TRACEPARENT"]
244
+ carrier["tracestate"] = ENV["TRACESTATE"] if ENV["TRACESTATE"]
245
+ OpenTelemetry.propagation.extract(carrier)
246
+ end
247
+
248
+ def default_tracer_provider(tenant = nil)
249
+ unless OpenTelemetry.tracer_provider.is_a?(OpenTelemetry::SDK::Trace::TracerProvider)
250
+ OpenTelemetry::SDK.configure do |config|
251
+ config.service_name = ENV["OTEL_SERVICE_NAME"] || "chef-client"
252
+ # Configurator#resource= merges into the default resource, which
253
+ # already carries OTEL_RESOURCE_ATTRIBUTES, so an explicit tenant
254
+ # wins over the env var.
255
+ config.resource = OpenTelemetry::SDK::Resources::Resource.create("__tenant" => tenant.to_s) if tenant
256
+ end
257
+ end
258
+ OpenTelemetry.tracer_provider
259
+ end
260
+
261
+ # Tracing must never break the chef run: if any event method raises,
262
+ # log it and ignore all further events instead of letting the error
263
+ # propagate into the event dispatcher. Defined last so it wraps every
264
+ # event method above.
265
+ crash_guard = Module.new do
266
+ Handler.public_instance_methods(false).each do |event|
267
+ define_method(event) do |*args|
268
+ return if @broken
269
+
270
+ begin
271
+ super(*args)
272
+ rescue Exception => e # rubocop:disable Lint/RescueException
273
+ @broken = true
274
+ Chef::Log.warn("OTel tracing handler failed (#{e.class}: #{e.message}); tracing disabled for the rest of this run")
275
+ end
276
+ end
277
+ end
278
+ end
279
+ prepend crash_guard
280
+ end
281
+ end
@@ -0,0 +1,21 @@
1
+ #
2
+ # Author:: Jason Cook (<jasonc@simpleideas.org>)
3
+ # Copyright:: Copyright (c) Jason Cook
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module CincOtel
20
+ VERSION = "1.0.0".freeze
21
+ end
data/lib/cinc_otel.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "cinc_otel/version"
4
+ require_relative "cinc_otel/handler"
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cinc-otel
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jason Cook
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-01 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: opentelemetry-sdk
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.4'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.4'
26
+ - !ruby/object:Gem::Dependency
27
+ name: opentelemetry-exporter-otlp
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.29'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.29'
40
+ description: |
41
+ Emits an OpenTelemetry span tree for each Cinc Client run: a chef.run root
42
+ span, a child span for each run phase, and one span per resource action.
43
+ Implemented as a Chef::EventDispatch handler. Loaded inside a cinc-client
44
+ process; OpenTelemetry exporter and endpoint are configured through the
45
+ standard OTEL_* environment variables.
46
+ email:
47
+ - jasonc@simpleideas.org
48
+ executables: []
49
+ extensions: []
50
+ extra_rdoc_files: []
51
+ files:
52
+ - CHANGELOG.md
53
+ - LICENSE
54
+ - README.md
55
+ - lib/cinc-otel.rb
56
+ - lib/cinc_otel.rb
57
+ - lib/cinc_otel/handler.rb
58
+ - lib/cinc_otel/version.rb
59
+ homepage: https://github.com/fastly/cinc-otel
60
+ licenses:
61
+ - Apache-2.0
62
+ metadata:
63
+ homepage_uri: https://github.com/fastly/cinc-otel
64
+ source_code_uri: https://github.com/fastly/cinc-otel
65
+ changelog_uri: https://github.com/fastly/cinc-otel/blob/main/CHANGELOG.md
66
+ rubygems_mfa_required: 'true'
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '3.1'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubygems_version: 3.6.6
82
+ specification_version: 4
83
+ summary: OpenTelemetry run tracing for Cinc Client
84
+ test_files: []