Skip to content

Commit

Permalink
Add helper to create custom markers (#1378)
Browse files Browse the repository at this point in the history
Add a helper class that calls our Public Endpoint API to create custom
markers.

This helper is added to give people a method to report custom markers
from the Ruby gem. We have chosen to do this through the public endpoint
and not the Push API, because it's better fits the data flow we want.

It does not support reporting deploy markers, because we only want to
report those through the `revision` config option.

This request is authenticated with the Push API key, app name and app
environment as query parameters, which is supported by the Public
Endpoint API, similar to how we report check ins.
The 'normal' API requires a Personal Access Token, which gives a lot of
access to the reported data. This method uses a write only
authentication method.

Related work:

- Public Endpoint PR:
  appsignal/appsignal-processor-rs#1679
- Docs PR:
  appsignal/appsignal-docs#2419

Closes #1375
  • Loading branch information
tombruijn authored Feb 21, 2025
1 parent f934d0d commit e92c8c9
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
bump: minor
type: add
---

Add a helper to create custom markers from the Ruby gem.

Create a custom marker (a little icon shown in the graph timeline on AppSignal.com) to mark events on the timeline.

Create a marker with all the available options:

```ruby
Appsignal::CustomMarker.report(
# The icon shown on the timeline
:icon => "🎉",
# The message shown on hover
:message => "Migration completed",
# Any time object or a string with a ISO8601 valid time is accepted
:created_at => Time.now
)
```

Create a marker with just a message:

```ruby
Appsignal::CustomMarker.report(
:message => "Migration completed",
)
```

_The default icon is the 🚀 icon. The default time is the time the request is received by our servers._
1 change: 1 addition & 0 deletions lib/appsignal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,7 @@ def collect_environment_metadata
require "appsignal/hooks"
require "appsignal/probes"
require "appsignal/marker"
require "appsignal/custom_marker"
require "appsignal/garbage_collection"
require "appsignal/rack"
require "appsignal/rack/body_wrapper"
Expand Down
72 changes: 72 additions & 0 deletions lib/appsignal/custom_marker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# frozen_string_literal: true

module Appsignal
# Custom markers are used on AppSignal.com to indicate events in an
# application, to give additional context on graph timelines.
#
# This helper class will send a request to the AppSignal public endpoint to
# create a Custom marker for the application on AppSignal.com.
#
# @see https://docs.appsignal.com/api/public-endpoint/custom-markers.html
# Public Endpoint API markers endpoint documentation
# @see https://docs.appsignal.com/appsignal/terminology.html#markers
# Terminology: Markers
class CustomMarker
# @param icon [String] icon to use for the marker, like an emoji.
# @param message [String] name of the user that is creating the
# marker.
# @param created_at [Time/String] A Ruby time object or a valid ISO8601
# timestamp.
# @return [Boolean]
def self.report(
icon: nil,
message: nil,
created_at: nil
)
new(
{
:icon => icon,
:message => message,
:created_at => created_at.respond_to?(:iso8601) ? created_at.iso8601 : created_at
}.compact
).transmit
end

# @api private
def initialize(marker_data)
@marker_data = marker_data
end

# @api private
def transmit
unless Appsignal.config
Appsignal.internal_logger.warn(
"Did not transmit custom marker: no AppSignal config loaded"
)
return false
end

transmitter = Transmitter.new(
"#{Appsignal.config[:logging_endpoint]}/markers",
Appsignal.config
)
response = transmitter.transmit(@marker_data)

if (200...300).include?(response.code.to_i)
Appsignal.internal_logger.info("Transmitted custom marker")
true
else
Appsignal.internal_logger.error(
"Failed to transmit custom marker: #{response.code} status code"
)
false
end
rescue => e
Appsignal.internal_logger.error(
"Failed to transmit custom marker: #{e.class}: #{e.message}\n" \
"#{e.backtrace}"
)
false
end
end
end
155 changes: 155 additions & 0 deletions spec/lib/appsignal/custom_marker_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
describe Appsignal::CustomMarker do
let(:config) { build_config }

describe "#transmit" do
# def stub_marker_request
# stub_api_request config, "markers", marker.marker_data
# end

def create_marker(
icon: nil,
message: nil,
created_at: nil
)
described_class.report(
:icon => icon,
:message => message,
:created_at => created_at
)
end

context "without Appsignal.config" do
it "logs a warning" do
logs =
capture_logs do
expect(create_marker(
:icon => "🎉",
:message => "Migration completed",
:created_at => Time.now
)).to be(false)
end
expect(logs)
.to contains_log(:warn, "Did not transmit custom marker: no AppSignal config loaded")
end
end

context "with Appsignal.config" do
before { configure }

context "when request is valid" do
it "sends request with all parameters and logs success" do
time = "2025-02-21T11:03:48+01:00"
stub_public_endpoint_markers_request(
:marker_data => {
"icon" => "🎉",
"message" => "Migration completed",
"created_at" => time
}
).to_return(:status => 200)

logs =
capture_logs do
expect(create_marker(
:icon => "🎉",
:message => "Migration completed",
:created_at => time
)).to be(true)
end

expect(logs).to contains_log(:info, "Transmitted custom marker")
expect(logs).to_not contains_log(:error, "Failed to transmit custom marker")
end

it "sends request with time object as parameter and logs success" do
time = Time.now.utc
stub_public_endpoint_markers_request(
:marker_data => {
"icon" => "🎉",
"message" => "Migration completed",
"created_at" => time.iso8601
}
).to_return(:status => 200)

logs =
capture_logs do
expect(create_marker(
:icon => "🎉",
:message => "Migration completed",
:created_at => time
)).to be(true)
end

expect(logs).to contains_log(:info, "Transmitted custom marker")
expect(logs).to_not contains_log(:error, "Failed to transmit custom marker")
end

it "sends request with some parameters and logs success" do
stub_public_endpoint_markers_request(
:marker_data => {
"message" => "Migration completed"
}
).to_return(:status => 200)

logs =
capture_logs do
expect(create_marker(:message => "Migration completed")).to be(true)
end

expect(logs).to contains_log(:info, "Transmitted custom marker")
expect(logs).to_not contains_log(:error, "Failed to transmit custom marker")
end
end

context "when request failed" do
it "logs error" do
time = Time.now.utc
stub_public_endpoint_markers_request(
:marker_data => {
"icon" => "🎉",
"message" => "Migration completed",
"created_at" => time.iso8601
}
).to_return(:status => 500)

logs =
capture_logs do
expect(create_marker(
:icon => "🎉",
:message => "Migration completed",
:created_at => time
)).to be(false)
end

expect(logs).to_not contains_log(:info, "Transmitted custom marker")
expect(logs).to contains_log(:error, "Failed to transmit custom marker: 500 status code")
end
end

context "when request raised an error" do
it "logs error" do
time = Time.now.utc
stub_public_endpoint_markers_request(
:marker_data => {
"icon" => "🎉",
"message" => "Migration completed",
"created_at" => time.iso8601
}
).to_raise(RuntimeError.new("uh oh"))

logs =
capture_logs do
expect(create_marker(
:icon => "🎉",
:message => "Migration completed",
:created_at => time
)).to be(false)
end

expect(logs).to_not contains_log(:info, "Transmitted custom marker")
expect(logs)
.to contains_log(:error, "Failed to transmit custom marker: RuntimeError: uh oh")
end
end
end
end
end
21 changes: 21 additions & 0 deletions spec/support/helpers/api_request_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,27 @@ def stub_api_request(config, path, body = nil)
stub_request(:post, "#{endpoint}/1/#{path}").with(options)
end

def stub_public_endpoint_markers_request(marker_data:)
config = Appsignal.config
options = {
:query => {
:api_key => config[:push_api_key],
:name => config[:name],
:environment => config.respond_to?(:env) ? config.env : config[:environment],
:hostname => config[:hostname],
:gem_version => Appsignal::VERSION
},
:headers => { "Content-Type" => "application/json; charset=UTF-8" }
}
stub_request(
:post,
"#{config[:logging_endpoint]}/markers"
).with(options) do |request|
payload = JSON.parse(request.body)
expect(payload).to match(marker_data)
end
end

def stub_cron_check_in_request(events:, response: { :status => 200 })
stub_check_in_requests(
:requests => [events],
Expand Down

0 comments on commit e92c8c9

Please sign in to comment.