polyn 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b87ad4ec53f26f70e9c6bb82ee6605ef19a9fa45452f312533fc43db4b91d03d
4
- data.tar.gz: ab02358b23de26f8db8c744d70d5d59e4d11ce0792c766e3de8b65af3acde143
3
+ metadata.gz: f3ccd094b68ccf93066ade4f7b3348e1048be916d1f220c4602abbf1884855f6
4
+ data.tar.gz: 06c13d39dbf4804d41a57754816338271ff2b1cdfdb4332cddfd46f07ecc3451
5
5
  SHA512:
6
- metadata.gz: aea99bcf38e0a88f744eabb15340ab0c64761f91b052549fc933f48425e8a5d9d36bfec50ac9a8178b446056a95cd9b4f33e24226194f06fec9f16a77f483dba
7
- data.tar.gz: 23bdb00776b3c3dfd4b07ed52b291fcb9842c61549a4ed58f9696357da1d292e29fb6365ea7eadac5bf9b0752fbd85fbf74b1fb54294400ba55e06292b813eef
6
+ metadata.gz: 2e7d51c4bea9a4f3910e0b209c34dd6281dea5df05b1bef6bcb580c824c65c5a20776486787a91e706472833ffbdb13be83bf7f30fbabdd8f5dd8ef95e4ff15b
7
+ data.tar.gz: 2b800e2e958a7e30c41720576b0c96b7f1f3f654bb08e6b97198aa3f2364095d17849ec2dbe080387496b2bcf40b35795b8051d92d48315c821d3d8d93cc7119
data/Gemfile CHANGED
@@ -30,3 +30,9 @@ gem "timecop"
30
30
  gem "json_schemer"
31
31
  # EventMachine nats repo doesn't support jetstream, only nats-pure
32
32
  gem "nats-pure", "~> 2.0"
33
+ gem "opentelemetry-api", "~> 1.1"
34
+
35
+ group :test do
36
+ gem "opentelemetry-sdk", "~> 1.2"
37
+ end
38
+
data/Gemfile.lock CHANGED
@@ -1,9 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- polyn (0.2.1)
4
+ polyn (0.3.0)
5
5
  json_schemer (~> 0.2)
6
6
  nats-pure (~> 2.0)
7
+ opentelemetry-api (~> 1.1)
7
8
  yard (~> 0.9)
8
9
 
9
10
  GEM
@@ -21,6 +22,18 @@ GEM
21
22
  regexp_parser (~> 2.0)
22
23
  uri_template (~> 0.7)
23
24
  nats-pure (2.1.2)
25
+ opentelemetry-api (1.1.0)
26
+ opentelemetry-common (0.19.6)
27
+ opentelemetry-api (~> 1.0)
28
+ opentelemetry-registry (0.2.0)
29
+ opentelemetry-api (~> 1.1)
30
+ opentelemetry-sdk (1.2.0)
31
+ opentelemetry-api (~> 1.1)
32
+ opentelemetry-common (~> 0.19.3)
33
+ opentelemetry-registry (~> 0.2)
34
+ opentelemetry-semantic_conventions
35
+ opentelemetry-semantic_conventions (1.8.0)
36
+ opentelemetry-api (~> 1.0)
24
37
  parallel (1.21.0)
25
38
  parser (3.0.2.0)
26
39
  ast (~> 2.4.1)
@@ -75,6 +88,8 @@ PLATFORMS
75
88
  DEPENDENCIES
76
89
  json_schemer
77
90
  nats-pure (~> 2.0)
91
+ opentelemetry-api (~> 1.1)
92
+ opentelemetry-sdk (~> 1.2)
78
93
  polyn!
79
94
  rake (~> 13.0)
80
95
  rspec (~> 3.0)
data/README.md CHANGED
@@ -78,15 +78,6 @@ Add `:source` to make the `source` of the event more specific
78
78
  polyn.publish("user.created.v1", { name: "Mary" }, source: "new.users")
79
79
  ```
80
80
 
81
- Add `:triggered_by` to add a triggering event to the `polyntrace`
82
-
83
- ```ruby
84
- polyn = Polyn.connect(nats)
85
-
86
- event = Polyn::Event.new
87
- polyn.publish("user.created.v1", { name: "Mary" }, triggered_by: event)
88
- ```
89
-
90
81
  You can also include options of `:header` and/or `:reply_to` to passthrough to NATS
91
82
 
92
83
  ### Consuming a Stream
@@ -165,6 +156,12 @@ Despite mocking some NATS functionality you will still need a running nats-serve
165
156
  When the tests start it will load all your schemas. The tests themselves will also use the running server to verify
166
157
  stream and consumer configuration information. This hybrid mocking approach is intended to give isolation and reliability while also ensuring correct integration.
167
158
 
159
+ ## Observability
160
+
161
+ ### Tracing
162
+
163
+ Polyn uses [OpenTelemetry](https://opentelemetry.io/) to create distributed traces that will connect sent and received events in different services. Your application will need the [`opentelemetry-sdk` gem](https://opentelemetry.io/docs/instrumentation/ruby/getting-started/) installed to collect the trace information.
164
+
168
165
  ## Development
169
166
 
170
167
  After checking out the repo, run `bin/setup` to install dependencies. Then, run
@@ -13,7 +13,7 @@
13
13
  "source": {
14
14
  "description": "Identifies the context in which an event happened.",
15
15
  "$ref": "#/definitions/sourcedef",
16
- "examples" : [
16
+ "examples": [
17
17
  "https://github.com/cloudevents",
18
18
  "mailto:cncf-wg-serverless@lists.cncf.io",
19
19
  "urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66",
@@ -32,7 +32,7 @@
32
32
  "type": {
33
33
  "description": "Describes the type of event related to the originating occurrence.",
34
34
  "$ref": "#/definitions/typedef",
35
- "examples" : [
35
+ "examples": [
36
36
  "com.github.pull_request.opened",
37
37
  "com.example.object.deleted.v2"
38
38
  ]
@@ -89,22 +89,14 @@
89
89
  "clientversion": "0.1.0"
90
90
  }
91
91
  ]
92
- },
93
- "polyntrace": {
94
- "$ref": "#/definitions/polyntracedef",
95
- "description": "Previous events that led to this one",
96
- "examples": [
97
- [
98
- {
99
- "type": "<topic>",
100
- "time": "2018-04-05T17:31:00Z",
101
- "id": "<uuid>"
102
- }
103
- ]
104
- ]
105
92
  }
106
93
  },
107
- "required": ["id", "source", "specversion", "type"],
94
+ "required": [
95
+ "id",
96
+ "source",
97
+ "specversion",
98
+ "type"
99
+ ],
108
100
  "definitions": {
109
101
  "iddef": {
110
102
  "type": "string",
@@ -124,28 +116,50 @@
124
116
  "minLength": 1
125
117
  },
126
118
  "datacontenttypedef": {
127
- "type": ["string", "null"],
119
+ "type": [
120
+ "string",
121
+ "null"
122
+ ],
128
123
  "minLength": 1
129
124
  },
130
125
  "dataschemadef": {
131
- "type": ["string", "null"],
126
+ "type": [
127
+ "string",
128
+ "null"
129
+ ],
132
130
  "format": "uri",
133
131
  "minLength": 1
134
132
  },
135
133
  "subjectdef": {
136
- "type": ["string", "null"],
134
+ "type": [
135
+ "string",
136
+ "null"
137
+ ],
137
138
  "minLength": 1
138
139
  },
139
140
  "timedef": {
140
- "type": ["string", "null"],
141
+ "type": [
142
+ "string",
143
+ "null"
144
+ ],
141
145
  "format": "date-time",
142
146
  "minLength": 1
143
147
  },
144
148
  "datadef": {
145
- "type": ["object", "string", "number", "array", "boolean", "null"]
149
+ "type": [
150
+ "object",
151
+ "string",
152
+ "number",
153
+ "array",
154
+ "boolean",
155
+ "null"
156
+ ]
146
157
  },
147
158
  "data_base64def": {
148
- "type": ["string", "null"],
159
+ "type": [
160
+ "string",
161
+ "null"
162
+ ],
149
163
  "contentEncoding": "base64"
150
164
  },
151
165
  "polyndatadef": {
@@ -161,27 +175,11 @@
161
175
  "type": "string"
162
176
  }
163
177
  },
164
- "required": ["clientlang", "clientlangversion", "clientversion"]
165
- },
166
- "polyntracedef": {
167
- "type" : "array",
168
- "items": {
169
- "type": "object",
170
- "properties": {
171
- "type": {
172
- "type": "string"
173
- },
174
- "time": {
175
- "type": "string",
176
- "format": "date-time"
177
- },
178
- "id" : {
179
- "type": "string",
180
- "format": "uuid"
181
- }
182
- },
183
- "required": ["type", "time", "id"]
184
- }
178
+ "required": [
179
+ "clientlang",
180
+ "clientlangversion",
181
+ "clientversion"
182
+ ]
185
183
  }
186
184
  }
187
185
  }
data/lib/polyn/event.rb CHANGED
@@ -76,7 +76,6 @@ module Polyn
76
76
  @time = hash.fetch(:time, Time.now.utc.iso8601)
77
77
  @data = hash.fetch(:data)
78
78
  @datacontenttype = hash.fetch(:datacontenttype, "application/json")
79
- @polyntrace = self.class.build_polyntrace(hash[:triggered_by])
80
79
  @polyndata = {
81
80
  clientlang: "ruby",
82
81
  clientlangversion: RUBY_VERSION,
@@ -93,7 +92,6 @@ module Polyn
93
92
  "time" => time,
94
93
  "data" => Utils::Hash.deep_stringify_keys(data),
95
94
  "datacontenttype" => datacontenttype,
96
- "polyntrace" => Utils::Hash.deep_stringify_keys(polyntrace),
97
95
  "polyndata" => Utils::Hash.deep_stringify_keys(polyndata),
98
96
  }
99
97
  end
@@ -101,15 +99,20 @@ module Polyn
101
99
  ##
102
100
  # Get the Event `source` prefixed with reverse domain name
103
101
  def self.full_source(source = nil)
104
- root = Polyn.configuration.source_root
105
- name = Polyn::Naming.dot_to_colon("#{domain}:#{root}")
102
+ root = Polyn.configuration.source_root
103
+ parts = [domain, root]
104
+ combine = lambda do |items|
105
+ items.map { |part| Polyn::Naming.dot_to_colon(part) }.join(":")
106
+ end
107
+ name = combine.call(parts)
106
108
 
107
109
  if source
108
110
  Polyn::Naming.validate_source_name!(source)
109
- "#{name}:#{Polyn::Naming.dot_to_colon(source)}"
110
- else
111
- name
111
+ source = source.gsub(/(#{name}){1}:?/, "")
112
+ parts << source unless source.empty?
112
113
  end
114
+
115
+ combine.call(parts)
113
116
  end
114
117
 
115
118
  ##
@@ -119,18 +122,6 @@ module Polyn
119
122
  "#{domain}.#{Polyn::Naming.trim_domain_prefix(type)}"
120
123
  end
121
124
 
122
- ##
123
- # Use a triggering event to build the polyntrace of a new event
124
- def self.build_polyntrace(triggered_by)
125
- return [] unless triggered_by
126
-
127
- triggered_by.polyntrace.concat([{
128
- id: triggered_by.id,
129
- type: triggered_by.type,
130
- time: triggered_by.time,
131
- }])
132
- end
133
-
134
125
  def self.domain
135
126
  Polyn.configuration.domain
136
127
  end
data/lib/polyn/nats.rb CHANGED
@@ -8,6 +8,8 @@ module Polyn
8
8
  @nats = nats
9
9
  end
10
10
 
11
+ attr_reader :nats
12
+
11
13
  def publish(type, json, reply, **opts)
12
14
  @nats.publish(type, json, reply, **opts)
13
15
  end
@@ -11,8 +11,7 @@ module Polyn
11
11
  # is more than the `source_root`
12
12
  def initialize(fields)
13
13
  @nats = fields.fetch(:nats)
14
- @type = fields.fetch(:type)
15
- @type = Polyn::Naming.trim_domain_prefix(@type)
14
+ @type = Polyn::Naming.trim_domain_prefix(fields.fetch(:type))
16
15
  @consumer_name = Polyn::Naming.consumer_name(@type, fields[:source])
17
16
  @stream = @nats.jetstream.find_stream_name_by_subject(@type)
18
17
  self.class.validate_consumer_exists!(@nats, @stream, @consumer_name)
@@ -39,18 +38,35 @@ module Polyn
39
38
  # @option params [Float] :timeout Duration of the fetch request before it expires.
40
39
  # @return [Array<NATS::Msg>]
41
40
  def fetch(batch = 1, params = {})
42
- msgs = @psub.fetch(batch, params)
43
- msgs.map do |msg|
44
- msg = Polyn::Nats::Msg.new(msg)
45
- event = @serializer.deserialize(msg.data)
46
- if event.is_a?(Polyn::Errors::Error)
47
- msg.term
48
- raise event
41
+ Polyn::Tracing.processing_span(@type) do |process_span|
42
+ msgs = @psub.fetch(batch, params)
43
+ msgs.map do |msg|
44
+ Polyn::Tracing.subscribe_span(@type, msg, links: [process_span]) do |span|
45
+ updated_msg = process_message(msg)
46
+ Polyn::Tracing.span_attributes(span,
47
+ nats: @nats,
48
+ type: @type,
49
+ event: updated_msg.data,
50
+ payload: msg.data)
51
+ updated_msg
52
+ end
49
53
  end
54
+ end
55
+ end
50
56
 
51
- msg.data = event
52
- msg
57
+ private
58
+
59
+ def process_message(msg)
60
+ msg = msg.clone
61
+ msg = Polyn::Nats::Msg.new(msg)
62
+ event = @serializer.deserialize(msg.data)
63
+ if event.is_a?(Polyn::Errors::Error)
64
+ msg.term
65
+ raise event
53
66
  end
67
+
68
+ msg.data = event
69
+ msg
54
70
  end
55
71
  end
56
72
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polyn
4
+ ##
5
+ # Methods to enable distributed tracing across services
6
+ # Attempts to follow OpenTelemetry conventions
7
+ # https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/messaging/
8
+ class Tracing
9
+ ##
10
+ # Tracer object to use to start a trace
11
+ def self.tracer
12
+ ::OpenTelemetry.tracer_provider.tracer("polyn", Polyn::VERSION)
13
+ end
14
+
15
+ ##
16
+ # Common attributes to add to a span involving an individual message
17
+ # https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/messaging/#messaging-attributes
18
+ def self.span_attributes(span, nats:, type:, event:, payload:)
19
+ span.add_attributes({
20
+ "messaging.system" => "NATS",
21
+ "messaging.destination" => type,
22
+ "messaging.protocol" => "Polyn",
23
+ "messaging.url" => nats.uri.to_s,
24
+ "messaging.message_id" => event.id,
25
+ "messaging.message_payload_size_bytes" => payload.bytesize,
26
+ })
27
+ end
28
+
29
+ ##
30
+ # Uses the message header to extract trace information from the
31
+ # published message so the subscription handler can use it as
32
+ # the parent span. This will allow us to create a distributed
33
+ # trace between publications and subscriptions. It's expecting a
34
+ # `traceparent` header to be set on the message
35
+ # https://www.w3.org/TR/trace-context/#traceparent-header
36
+ def self.connect_span_with_received_message(msg, &block)
37
+ context = OpenTelemetry.propagation.extract(msg.header)
38
+ ::OpenTelemetry::Context.with_current(context, &block)
39
+ end
40
+
41
+ ##
42
+ # Add a `traceparent` header to the headers for a message so that the
43
+ # subscribers can be connected with it
44
+ # https://www.w3.org/TR/trace-context/#traceparent-header
45
+ def self.trace_header(headers = {})
46
+ ::OpenTelemetry.propagation.inject(headers)
47
+ end
48
+
49
+ ##
50
+ # Start a span for publishing an event
51
+ def self.publish_span(type, &block)
52
+ tracer.in_span("#{type} send", kind: "PRODUCER", &block)
53
+ end
54
+
55
+ ##
56
+ # Start a span for handling a received event
57
+ def self.subscribe_span(type, msg, links: nil, &block)
58
+ connect_span_with_received_message(msg) do
59
+ tracer.in_span("#{type} receive", kind: "CONSUMER", links: convert_links(links), &block)
60
+ end
61
+ end
62
+
63
+ ##
64
+ # Start a span to handle processing of batch messages
65
+ def self.processing_span(type, &block)
66
+ tracer.in_span("#{type} process", kind: "CONSUMER", &block)
67
+ end
68
+
69
+ class << self
70
+ private
71
+
72
+ def convert_links(links)
73
+ links.map { |link| create_span_link(link) } if links
74
+ end
75
+
76
+ def create_span_link(span)
77
+ ::OpenTelemetry::Trace::Link.new(span.context)
78
+ end
79
+ end
80
+ end
81
+ end
data/lib/polyn/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyn
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/polyn.rb CHANGED
@@ -21,6 +21,7 @@
21
21
  require "json_schemer"
22
22
  require "json"
23
23
  require "nats/client"
24
+ require "opentelemetry"
24
25
  require "securerandom"
25
26
 
26
27
  require "polyn/configuration"
@@ -34,6 +35,8 @@ require "polyn/nats/jetstream/api/consumer_config"
34
35
  require "polyn/pull_subscriber"
35
36
  require "polyn/schema_store"
36
37
  require "polyn/serializers/json"
38
+ require "polyn/testing/mock_nats"
39
+ require "polyn/tracing"
37
40
  require "polyn/utils/utils"
38
41
  require "polyn/version"
39
42
 
@@ -77,27 +80,29 @@ module Polyn
77
80
  # @param type [String] The type of event
78
81
  # @param data [any] The data to include in the event
79
82
  # @option options [String] :source - information to specify the source of the event
80
- # @option options [String] :triggered_by - The event that triggered this one.
81
83
  # Will use information from the event to build up the `polyntrace` data
82
84
  # @option options [String] :reply_to - Reply to a specific topic
83
85
  # @option options [String] :header - Headers to include in the message
84
86
  def publish(type, data, **opts)
85
- event = Event.new({
86
- type: type,
87
- source: opts[:source],
88
- data: data,
89
- triggered_by: opts[:triggered_by],
90
- })
87
+ Polyn::Tracing.publish_span(type) do |span|
88
+ event = Event.new({
89
+ type: type,
90
+ source: opts[:source],
91
+ data: data,
92
+ })
91
93
 
92
- # Ensure accidental message duplication doesn't happen
93
- # https://docs.nats.io/using-nats/developer/develop_jetstream/model_deep_dive#message-deduplication
94
- msg_id_header = { "Nats-Msg-Id" => event.id }
95
- header = opts.fetch(:header, {})
96
- header = msg_id_header.merge(header)
94
+ json = @serializer.serialize!(event)
97
95
 
98
- json = @serializer.serialize!(event)
96
+ Polyn::Tracing.span_attributes(span,
97
+ nats: @nats.nats,
98
+ type: type,
99
+ event: event,
100
+ payload: json)
99
101
 
100
- @nats.publish(type, json, opts[:reply_to], header: header)
102
+ header = add_headers(opts.fetch(:header, {}), event)
103
+
104
+ @nats.publish(type, json, opts[:reply_to], header: header)
105
+ end
101
106
  end
102
107
 
103
108
  ## Create subscription which is dispatched asynchronously
@@ -110,9 +115,18 @@ module Polyn
110
115
  # @option options [String] :pending_bytes_limit
111
116
  def subscribe(type, opts = {}, &callback)
112
117
  @nats.subscribe(type, opts) do |msg|
113
- event = @serializer.deserialize!(msg.data)
114
- msg.data = event
115
- callback.call(msg)
118
+ Polyn::Tracing.subscribe_span(type, msg) do |span|
119
+ event = @serializer.deserialize!(msg.data)
120
+
121
+ Polyn::Tracing.span_attributes(span,
122
+ nats: @nats.nats,
123
+ type: type,
124
+ event: event,
125
+ payload: msg.data)
126
+
127
+ msg.data = event
128
+ callback.call(msg)
129
+ end
116
130
  end
117
131
  end
118
132
 
@@ -148,10 +162,17 @@ module Polyn
148
162
  # NATS connection class to use based on environment
149
163
  def nats_class
150
164
  if Polyn.configuration.polyn_env == "test"
151
- Polyn::MockNats
165
+ Polyn::Testing::MockNats
152
166
  else
153
167
  Polyn::Nats
154
168
  end
155
169
  end
170
+
171
+ def add_headers(headers, event)
172
+ Polyn::Tracing.trace_header(headers)
173
+ # Ensure accidental message duplication doesn't happen
174
+ # https://docs.nats.io/using-nats/developer/develop_jetstream/model_deep_dive#message-deduplication
175
+ { "Nats-Msg-Id" => event.id }.merge(headers)
176
+ end
156
177
  end
157
178
  end
data/polyn.gemspec CHANGED
@@ -48,5 +48,6 @@ Gem::Specification.new do |spec|
48
48
 
49
49
  spec.add_dependency "json_schemer", "~> 0.2"
50
50
  spec.add_dependency "nats-pure", "~> 2.0"
51
+ spec.add_dependency "opentelemetry-api", "~> 1.1"
51
52
  spec.add_dependency "yard", "~> 0.9"
52
53
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: polyn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jarod
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2022-09-09 00:00:00.000000000 Z
12
+ date: 2022-10-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: json_schemer
@@ -39,6 +39,20 @@ dependencies:
39
39
  - - "~>"
40
40
  - !ruby/object:Gem::Version
41
41
  version: '2.0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: opentelemetry-api
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '1.1'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '1.1'
42
56
  - !ruby/object:Gem::Dependency
43
57
  name: yard
44
58
  requirement: !ruby/object:Gem::Requirement
@@ -99,6 +113,7 @@ files:
99
113
  - lib/polyn/testing/mock_msg.rb
100
114
  - lib/polyn/testing/mock_nats.rb
101
115
  - lib/polyn/testing/mock_pull_subscription.rb
116
+ - lib/polyn/tracing.rb
102
117
  - lib/polyn/utils/hash.rb
103
118
  - lib/polyn/utils/string.rb
104
119
  - lib/polyn/utils/utils.rb