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 +7 -0
- data/CHANGELOG.md +26 -0
- data/LICENSE +202 -0
- data/README.md +176 -0
- data/lib/cinc-otel.rb +3 -0
- data/lib/cinc_otel/handler.rb +281 -0
- data/lib/cinc_otel/version.rb +21 -0
- data/lib/cinc_otel.rb +4 -0
- metadata +84 -0
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,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
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: []
|