summaryrefslogtreecommitdiff
path: root/src/email
diff options
context:
space:
mode:
authorSaumit <justsaumit@protonmail.com>2025-09-27 02:14:26 +0530
committerSaumit <justsaumit@protonmail.com>2025-09-27 02:14:26 +0530
commit82e03978b89938219958032efb1448cc76baa181 (patch)
tree626f3e54d52ecd49be0ed3bee30abacc0453d081 /src/email
Initial snapshot - OpenTelemetry demo 2.1.3 -f
Diffstat (limited to 'src/email')
-rw-r--r--src/email/.dockerignore1
-rw-r--r--src/email/.ruby-version1
-rw-r--r--src/email/Dockerfile28
-rw-r--r--src/email/Gemfile18
-rw-r--r--src/email/Gemfile.lock304
-rw-r--r--src/email/README.md23
-rw-r--r--src/email/email_server.rb83
-rw-r--r--src/email/views/confirmation.erb53
8 files changed, 511 insertions, 0 deletions
diff --git a/src/email/.dockerignore b/src/email/.dockerignore
new file mode 100644
index 0000000..b43bf86
--- /dev/null
+++ b/src/email/.dockerignore
@@ -0,0 +1 @@
+README.md
diff --git a/src/email/.ruby-version b/src/email/.ruby-version
new file mode 100644
index 0000000..f989260
--- /dev/null
+++ b/src/email/.ruby-version
@@ -0,0 +1 @@
+3.4.4
diff --git a/src/email/Dockerfile b/src/email/Dockerfile
new file mode 100644
index 0000000..c2aa6d3
--- /dev/null
+++ b/src/email/Dockerfile
@@ -0,0 +1,28 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+
+FROM docker.io/library/ruby:3.4.4-alpine3.22 AS builder
+
+COPY ./src/email/Gemfile Gemfile
+COPY ./src/email/Gemfile.lock Gemfile.lock
+
+RUN apk update && \
+ apk add make gcc musl-dev gcompat && \
+ bundle install
+
+FROM docker.io/library/ruby:3.4.4-alpine3.22
+
+COPY --from=builder /usr/local/bundle/ /usr/local/bundle/
+
+WORKDIR /email_server
+
+COPY ./src/email/views/ views/
+
+COPY ./src/email/.ruby-version .ruby-version
+COPY ./src/email/Gemfile Gemfile
+COPY ./src/email/Gemfile.lock Gemfile.lock
+COPY ./src/email/email_server.rb email_server.rb
+
+EXPOSE ${EMAIL_PORT}
+ENTRYPOINT ["bundle", "exec", "ruby", "email_server.rb"]
diff --git a/src/email/Gemfile b/src/email/Gemfile
new file mode 100644
index 0000000..7d2890a
--- /dev/null
+++ b/src/email/Gemfile
@@ -0,0 +1,18 @@
+
+
+source "https://rubygems.org"
+
+gem "net-smtp", "~> 0.5.1"
+gem "pony", "~> 1.13"
+gem "puma", "~> 7.0"
+gem "sinatra", "~> 4.1"
+gem "rackup", "~> 2.2"
+
+gem "google-protobuf", "~> 4.32.0"
+
+gem "opentelemetry-sdk", "~> 1.8"
+gem "opentelemetry-exporter-otlp", "~> 0.30.0"
+gem "opentelemetry-instrumentation-all", "~> 0.80.0"
+
+gem "openfeature-sdk", "~> 0.4"
+gem "openfeature-flagd-provider", "~> 0.1"
diff --git a/src/email/Gemfile.lock b/src/email/Gemfile.lock
new file mode 100644
index 0000000..53e6da2
--- /dev/null
+++ b/src/email/Gemfile.lock
@@ -0,0 +1,304 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ base64 (0.2.0)
+ bigdecimal (3.2.2)
+ date (3.4.1)
+ google-protobuf (4.32.0-aarch64-linux-musl)
+ bigdecimal
+ rake (>= 13)
+ google-protobuf (4.32.0-arm64-darwin)
+ bigdecimal
+ rake (>= 13)
+ google-protobuf (4.32.0-x86_64-darwin)
+ bigdecimal
+ rake (>= 13)
+ google-protobuf (4.32.0-x86_64-linux-musl)
+ bigdecimal
+ rake (>= 13)
+ googleapis-common-protos-types (1.20.0)
+ google-protobuf (>= 3.18, < 5.a)
+ logger (1.7.0)
+ mail (2.8.1)
+ mini_mime (>= 0.1.1)
+ net-imap
+ net-pop
+ net-smtp
+ mini_mime (1.1.5)
+ mustermann (3.0.3)
+ ruby2_keywords (~> 0.0.1)
+ net-imap (0.5.8)
+ date
+ net-protocol
+ net-pop (0.1.2)
+ net-protocol
+ net-protocol (0.2.2)
+ timeout
+ net-smtp (0.5.1)
+ net-protocol
+ nio4r (2.7.4)
+ opentelemetry-api (1.6.0)
+ opentelemetry-common (0.22.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-exporter-otlp (0.30.0)
+ google-protobuf (>= 3.18)
+ googleapis-common-protos-types (~> 1.3)
+ opentelemetry-api (~> 1.1)
+ opentelemetry-common (~> 0.20)
+ opentelemetry-sdk (~> 1.2)
+ opentelemetry-semantic_conventions
+ opentelemetry-helpers-mysql (0.2.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-common (~> 0.21)
+ opentelemetry-helpers-sql (0.1.1)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-helpers-sql-obfuscation (0.3.0)
+ opentelemetry-common (~> 0.21)
+ opentelemetry-instrumentation-action_mailer (0.4.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-active_support (~> 0.7)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-action_pack (0.13.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-rack (~> 0.21)
+ opentelemetry-instrumentation-action_view (0.9.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-active_support (~> 0.7)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-active_job (0.8.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-active_model_serializers (0.22.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-active_support (>= 0.7.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-active_record (0.9.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-active_storage (0.1.1)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-active_support (~> 0.7)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-active_support (0.8.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-all (0.80.0)
+ opentelemetry-instrumentation-active_model_serializers (~> 0.22.0)
+ opentelemetry-instrumentation-aws_lambda (~> 0.3.0)
+ opentelemetry-instrumentation-aws_sdk (~> 0.8.0)
+ opentelemetry-instrumentation-bunny (~> 0.22.0)
+ opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0)
+ opentelemetry-instrumentation-dalli (~> 0.27.0)
+ opentelemetry-instrumentation-delayed_job (~> 0.23.0)
+ opentelemetry-instrumentation-ethon (~> 0.23.0)
+ opentelemetry-instrumentation-excon (~> 0.24.0)
+ opentelemetry-instrumentation-faraday (~> 0.28.0)
+ opentelemetry-instrumentation-grape (~> 0.3.0)
+ opentelemetry-instrumentation-graphql (~> 0.29.0)
+ opentelemetry-instrumentation-grpc (~> 0.2.0)
+ opentelemetry-instrumentation-gruf (~> 0.3.0)
+ opentelemetry-instrumentation-http (~> 0.25.0)
+ opentelemetry-instrumentation-http_client (~> 0.24.0)
+ opentelemetry-instrumentation-httpx (~> 0.3.0)
+ opentelemetry-instrumentation-koala (~> 0.21.0)
+ opentelemetry-instrumentation-lmdb (~> 0.23.0)
+ opentelemetry-instrumentation-mongo (~> 0.23.0)
+ opentelemetry-instrumentation-mysql2 (~> 0.29.0)
+ opentelemetry-instrumentation-net_http (~> 0.23.0)
+ opentelemetry-instrumentation-pg (~> 0.30.0)
+ opentelemetry-instrumentation-que (~> 0.9.0)
+ opentelemetry-instrumentation-racecar (~> 0.4.0)
+ opentelemetry-instrumentation-rack (~> 0.27.0)
+ opentelemetry-instrumentation-rails (~> 0.37.0)
+ opentelemetry-instrumentation-rake (~> 0.3.1)
+ opentelemetry-instrumentation-rdkafka (~> 0.7.0)
+ opentelemetry-instrumentation-redis (~> 0.26.1)
+ opentelemetry-instrumentation-resque (~> 0.6.0)
+ opentelemetry-instrumentation-restclient (~> 0.24.0)
+ opentelemetry-instrumentation-ruby_kafka (~> 0.22.0)
+ opentelemetry-instrumentation-sidekiq (~> 0.26.0)
+ opentelemetry-instrumentation-sinatra (~> 0.26.0)
+ opentelemetry-instrumentation-trilogy (~> 0.61.0)
+ opentelemetry-instrumentation-aws_lambda (0.3.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-aws_sdk (0.8.2)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-base (0.23.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-common (~> 0.21)
+ opentelemetry-registry (~> 0.1)
+ opentelemetry-instrumentation-bunny (0.22.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-concurrent_ruby (0.22.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-dalli (0.27.3)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-delayed_job (0.23.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-ethon (0.23.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-excon (0.24.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-faraday (0.28.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-grape (0.3.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-rack (~> 0.21)
+ opentelemetry-instrumentation-graphql (0.29.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-grpc (0.2.1)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-gruf (0.3.0)
+ opentelemetry-api (>= 1.0.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-http (0.25.1)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-http_client (0.24.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-httpx (0.3.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-koala (0.21.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-lmdb (0.23.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-mongo (0.23.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-mysql2 (0.29.1)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-helpers-mysql
+ opentelemetry-helpers-sql
+ opentelemetry-helpers-sql-obfuscation
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-net_http (0.23.1)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-pg (0.30.1)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-helpers-sql
+ opentelemetry-helpers-sql-obfuscation
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-que (0.9.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-racecar (0.4.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-rack (0.27.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-rails (0.37.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-action_mailer (~> 0.4.0)
+ opentelemetry-instrumentation-action_pack (~> 0.13.0)
+ opentelemetry-instrumentation-action_view (~> 0.9.0)
+ opentelemetry-instrumentation-active_job (~> 0.8.0)
+ opentelemetry-instrumentation-active_record (~> 0.9.0)
+ opentelemetry-instrumentation-active_storage (~> 0.1.0)
+ opentelemetry-instrumentation-active_support (~> 0.8.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0)
+ opentelemetry-instrumentation-rake (0.3.1)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-rdkafka (0.7.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-redis (0.26.1)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-resque (0.6.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-restclient (0.24.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-ruby_kafka (0.22.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-sidekiq (0.26.1)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-sinatra (0.26.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-instrumentation-rack (~> 0.21)
+ opentelemetry-instrumentation-trilogy (0.61.1)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-helpers-mysql
+ opentelemetry-helpers-sql
+ opentelemetry-helpers-sql-obfuscation
+ opentelemetry-instrumentation-base (~> 0.23.0)
+ opentelemetry-semantic_conventions (>= 1.8.0)
+ opentelemetry-registry (0.4.0)
+ opentelemetry-api (~> 1.1)
+ opentelemetry-sdk (1.8.1)
+ opentelemetry-api (~> 1.1)
+ opentelemetry-common (~> 0.20)
+ opentelemetry-registry (~> 0.2)
+ opentelemetry-semantic_conventions
+ opentelemetry-semantic_conventions (1.11.0)
+ opentelemetry-api (~> 1.0)
+ pony (1.13.1)
+ mail (>= 2.0)
+ puma (7.0.0)
+ nio4r (~> 2.0)
+ rack (3.1.15)
+ rack-protection (4.1.1)
+ base64 (>= 0.1.0)
+ logger (>= 1.6.0)
+ rack (>= 3.0.0, < 4)
+ rack-session (2.1.1)
+ base64 (>= 0.1.0)
+ rack (>= 3.0.0)
+ rackup (2.2.1)
+ rack (>= 3)
+ rake (13.3.0)
+ ruby2_keywords (0.0.5)
+ sinatra (4.1.1)
+ logger (>= 1.6.0)
+ mustermann (~> 3.0)
+ rack (>= 3.0.0, < 4)
+ rack-protection (= 4.1.1)
+ rack-session (>= 2.0.0, < 3)
+ tilt (~> 2.0)
+ tilt (2.6.0)
+ timeout (0.4.3)
+
+PLATFORMS
+ aarch64-linux-musl
+ arm64-darwin
+ x86_64-darwin
+ x86_64-linux-musl
+
+DEPENDENCIES
+ google-protobuf (~> 4.32.0)
+ net-smtp (~> 0.5.1)
+ opentelemetry-exporter-otlp (~> 0.30.0)
+ opentelemetry-instrumentation-all (~> 0.80.0)
+ opentelemetry-sdk (~> 1.8)
+ pony (~> 1.13)
+ puma (~> 7.0)
+ rackup (~> 2.2)
+ sinatra (~> 4.1)
+
+BUNDLED WITH
+ 2.6.7
diff --git a/src/email/README.md b/src/email/README.md
new file mode 100644
index 0000000..13533d8
--- /dev/null
+++ b/src/email/README.md
@@ -0,0 +1,23 @@
+# Email Service
+
+The Email service "sends" an email to the customer with their order details by
+rendering it as a log message. It expects a JSON payload like:
+
+```json
+{
+ "email": "some.address@website.com",
+ "order": "<serialized order protobuf>"
+}
+```
+
+## Local Build
+
+We use `bundler` to manage dependencies. To get started, simply `bundle install`.
+
+## Running locally
+
+You may run this service locally with `bundle exec ruby email_server.rb`.
+
+## Docker Build
+
+From `src/email`, run `docker build .`
diff --git a/src/email/email_server.rb b/src/email/email_server.rb
new file mode 100644
index 0000000..98a5259
--- /dev/null
+++ b/src/email/email_server.rb
@@ -0,0 +1,83 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+require "ostruct"
+require "pony"
+require "sinatra"
+require "open_feature/sdk"
+require "openfeature/flagd/provider"
+# require "open_feature/flagd_provider"
+
+require "opentelemetry/sdk"
+require "opentelemetry/exporter/otlp"
+require "opentelemetry/instrumentation/sinatra"
+
+set :port, ENV["EMAIL_PORT"]
+
+# Initialize OpenFeature SDK with flagd provider
+flagd_client = OpenFeature::Flagd::Provider.build_client
+flagd_client.configure do |config|
+ config.host = ENV.fetch("FLAGD_HOST", "localhost")
+ config.port = ENV.fetch("FLAGD_PORT", 8013).to_i
+ config.tls = ENV.fetch("FLAGD_TLS", "false") == "true"
+end
+
+OpenFeature::SDK.configure do |config|
+ config.set_provider(flagd_client)
+end
+
+OpenTelemetry::SDK.configure do |c|
+ c.use "OpenTelemetry::Instrumentation::Sinatra"
+end
+
+post "/send_order_confirmation" do
+ data = JSON.parse(request.body.read, object_class: OpenStruct)
+
+ # get the current auto-instrumented span
+ current_span = OpenTelemetry::Trace.current_span
+ current_span.add_attributes({
+ "app.order.id" => data.order.order_id,
+ })
+
+ send_email(data)
+
+end
+
+error do
+ OpenTelemetry::Trace.current_span.record_exception(env['sinatra.error'])
+end
+
+def send_email(data)
+ # create and start a manual span
+ tracer = OpenTelemetry.tracer_provider.tracer('email')
+ tracer.in_span("send_email") do |span|
+ # Check if memory leak flag is enabled
+ client = OpenFeature::SDK.build_client
+ memory_leak_multiplier = client.fetch_number_value(flag_key: "emailMemoryLeak", default_value: 0)
+
+ # To speed up the memory leak we create a long email body
+ confirmation_content = erb(:confirmation, locals: { order: data.order })
+ whitespace_length = [0, confirmation_content.length * (memory_leak_multiplier-1)].max
+
+ Pony.mail(
+ to: data.email,
+ from: "noreply@example.com",
+ subject: "Your confirmation email",
+ body: confirmation_content + " " * whitespace_length,
+ via: :test
+ )
+
+ # If not clearing the deliveries, the emails will accumulate in the test mailer
+ # We use this to create a memory leak.
+ if memory_leak_multiplier < 1
+ Mail::TestMailer.deliveries.clear
+ end
+
+ span.set_attribute("app.email.recipient", data.email)
+ puts "Order confirmation email sent to: #{data.email}"
+ end
+ # manually created spans need to be ended
+ # in Ruby, the method `in_span` ends it automatically
+ # check out the OpenTelemetry Ruby docs at:
+ # https://opentelemetry.io/docs/instrumentation/ruby/manual/#creating-new-spans
+end
diff --git a/src/email/views/confirmation.erb b/src/email/views/confirmation.erb
new file mode 100644
index 0000000..aba297d
--- /dev/null
+++ b/src/email/views/confirmation.erb
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<!--
+ Copyright 2020 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<html>
+ <head>
+ <title>Your Order Confirmation</title>
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
+ </head>
+ <style>
+ body{
+ font-family: 'DM Sans', sans-serif;
+ }
+ </style>
+ <body>
+ <h2>Your Order Confirmation</h2>
+ <p>Thanks for shopping with us!<p>
+ <h3>Order ID</h3>
+ <p><%= order.order_id %></p>
+ <h3>Shipping</h3>
+ <p><%= order.shipping_tracking_id %></p>
+ <p><%= order.shipping_cost.units %>.<%= order.shipping_cost.nanos %> <%= order.shipping_cost.currency_code %></p>
+ <p><%= order.shipping_address.street_address_1 %>, <%= order.shipping_address.street_address_2 %>, <%= order.shipping_address.city %>, <%= order.shipping_address.country %> <%= order.shipping_address.zip_code %></p>
+ <h3>Items</h3>
+ <table style="width:100%">
+ <tr>
+ <th>Item No.</th>
+ <th>Quantity</th>
+ <th>Price</th>
+ </tr>
+ <% order.items.each do |item| %>
+ <tr>
+ <td><%= item.item.product_id %></td>
+ <td><%= item.item.quantity %></td>
+ <td><%= item.cost.units %>.<%= item.cost.nanos %> <%= item.cost.currency_code %></td>
+ </tr>
+ <% end %>
+ </table>
+ </body>
+</html>