logtail-rails 0.3.0 → 0.3.1

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.
@@ -4,8 +4,6 @@ on:
4
4
  push:
5
5
  branches: [main]
6
6
  pull_request:
7
- schedule:
8
- - cron: '20 5 * * 1'
9
7
  workflow_dispatch:
10
8
 
11
9
  jobs:
@@ -17,9 +15,11 @@ jobs:
17
15
  fail-fast: false
18
16
 
19
17
  matrix:
18
+ # Released Ruby and Rails versions only. The E2E workflow (e2e.yml) covers the newest stable Ruby and Rails,
19
+ # ruby-head and Rails' main branch.
20
20
  ruby-version:
21
21
  - "4.0"
22
- - "3"
22
+ - "3.4"
23
23
  - "3.3"
24
24
  - "3.2"
25
25
  - "3.1"
@@ -29,7 +29,6 @@ jobs:
29
29
  - "2.5"
30
30
  - "truffleruby"
31
31
  gemfile:
32
- - "rails-edge"
33
32
  - "rails-8.1"
34
33
  - "rails-8.0"
35
34
  - "rails-7.2"
@@ -41,19 +40,6 @@ jobs:
41
40
  - "rails-5.1"
42
41
  - "rails-5.0"
43
42
  exclude:
44
- - gemfile: "rails-edge"
45
- ruby-version: "3.2"
46
- - gemfile: "rails-edge"
47
- ruby-version: "3.1"
48
- - gemfile: "rails-edge"
49
- ruby-version: "3.0"
50
- - gemfile: "rails-edge"
51
- ruby-version: "2.7"
52
- - gemfile: "rails-edge"
53
- ruby-version: "2.6"
54
- - gemfile: "rails-edge"
55
- ruby-version: "2.5"
56
-
57
43
  - gemfile: "rails-8.1"
58
44
  ruby-version: "3.1"
59
45
  - gemfile: "rails-8.1"
@@ -98,7 +84,7 @@ jobs:
98
84
  - gemfile: "rails-5.2"
99
85
  ruby-version: "4.0"
100
86
  - gemfile: "rails-5.2"
101
- ruby-version: "3"
87
+ ruby-version: "3.4"
102
88
  - gemfile: "rails-5.2"
103
89
  ruby-version: "3.3"
104
90
  - gemfile: "rails-5.2"
@@ -113,7 +99,7 @@ jobs:
113
99
  - gemfile: "rails-5.1"
114
100
  ruby-version: "4.0"
115
101
  - gemfile: "rails-5.1"
116
- ruby-version: "3"
102
+ ruby-version: "3.4"
117
103
  - gemfile: "rails-5.1"
118
104
  ruby-version: "3.3"
119
105
  - gemfile: "rails-5.1"
@@ -128,7 +114,7 @@ jobs:
128
114
  - gemfile: "rails-5.0"
129
115
  ruby-version: "4.0"
130
116
  - gemfile: "rails-5.0"
131
- ruby-version: "3"
117
+ ruby-version: "3.4"
132
118
  - gemfile: "rails-5.0"
133
119
  ruby-version: "3.3"
134
120
  - gemfile: "rails-5.0"
@@ -0,0 +1,153 @@
1
+ name: "Notify Linear on failure"
2
+
3
+ # Files a Linear issue for a failed run: "E2E failure: <workflow name> (<owner/repo>)" at High priority, listing the
4
+ # failed jobs and steps with their errors, or with their summaries when job_summaries is set. When every failed job's
5
+ # name starts with "canary" (pre-release runtime or dependencies), the title says "E2E canary failure" and the
6
+ # priority is Low. While an issue with the same title is still open, the run is added to it as a comment instead,
7
+ # so a test that keeps failing every day makes one issue, not one per day.
8
+ # Secrets: LINEAR_API_KEY, LINEAR_TEAM_TELEMETRY_ID, LINEAR_STATUS_PLAN_ID, LINEAR_PROJECT_INGESTION_FOR_EVERYTHING_ID,
9
+ # LINEAR_ASSIGNEE_PETR_ID.
10
+ on:
11
+ workflow_call:
12
+ inputs:
13
+ job_summaries:
14
+ description: >-
15
+ Adds each failed job's summary to the issue, or its errors when it has none. A job hands its summary over as
16
+ an artifact named "e2e-summary-<job name>" holding summary.md.
17
+ type: boolean
18
+ default: false
19
+
20
+ jobs:
21
+ create-linear-issue:
22
+ runs-on: ubuntu-latest
23
+ # A caller must grant checks: read as well (the errors come from the annotations), or this job cannot start.
24
+ permissions:
25
+ contents: read
26
+ actions: read
27
+ checks: read
28
+ steps:
29
+ - name: "Gather failure context"
30
+ id: ctx
31
+ env:
32
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33
+ RUN_ID: ${{ github.event.workflow_run.id || github.run_id }}
34
+ RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt || github.run_attempt }}
35
+ JOB_SUMMARIES: ${{ inputs.job_summaries }}
36
+ run: |
37
+ JOBS=$(gh api \
38
+ "repos/${{ github.repository }}/actions/runs/$RUN_ID/attempts/$RUN_ATTEMPT/jobs" \
39
+ --paginate \
40
+ --jq '.jobs[]
41
+ | select(.conclusion == "failure")
42
+ | {id, name, steps: ([.steps[] | select(.conclusion == "failure") | .name] | join(", "))}
43
+ | @json')
44
+
45
+ FAILED=""
46
+ while read -r job; do
47
+ [ -n "$job" ] || continue
48
+ ID=$(jq -r .id <<<"$job")
49
+ NAME=$(jq -r .name <<<"$job")
50
+ STEPS=$(jq -r .steps <<<"$job")
51
+ FAILED="$FAILED### $NAME"$'\n\n'"Failed at **${STEPS}**"$'\n\n'
52
+ # A job's summary already says why it failed. Otherwise, or when the job stopped before writing it, the
53
+ # issue gets its ::error:: lines, without the runner's closing "Process completed with exit code 1."
54
+ SUMMARY_DIR="summaries/$ID"
55
+ if [ "$JOB_SUMMARIES" = true ]; then
56
+ gh run download "$RUN_ID" -R "${{ github.repository }}" -n "e2e-summary-$NAME" -D "$SUMMARY_DIR" >/dev/null 2>&1 || true
57
+ fi
58
+ if [ -f "$SUMMARY_DIR/summary.md" ]; then
59
+ FAILED="$FAILED$(cat "$SUMMARY_DIR/summary.md")"$'\n\n'
60
+ else
61
+ ERRORS=$(gh api "repos/${{ github.repository }}/check-runs/$ID/annotations" \
62
+ --jq '[.[] | select(.annotation_level == "failure" and (.message | startswith("Process completed with exit code") | not)) | .message | gsub("\n"; " ")] | unique | .[] | "- \(.)"') || ERRORS=""
63
+ FAILED="$FAILED${ERRORS:+$ERRORS$'\n\n'}"
64
+ fi
65
+ done <<<"$JOBS"
66
+ # Canary only when there are failed jobs and all of them are canary jobs.
67
+ CANARY=false
68
+ if [ -n "$JOBS" ] && jq -se 'all(.[]; .name | startswith("canary"))' <<<"$JOBS" >/dev/null; then CANARY=true; fi
69
+
70
+ while [[ "$FAILED" == *$'\n' ]]; do FAILED="${FAILED%$'\n'}"; done
71
+ if [ -z "$FAILED" ]; then
72
+ FAILED="_(no failed jobs reported by the API yet)_"
73
+ elif [ "${#FAILED}" -gt 20000 ]; then
74
+ FAILED="${FAILED:0:20000}"$'\n\n'"_(cut at 20,000 characters, the rest is in the run)_"
75
+ fi
76
+ printf '%s\n' "$FAILED" > failed_jobs.md
77
+ echo "canary=$CANARY" >> "$GITHUB_OUTPUT"
78
+
79
+ - name: "Create the Linear issue, or comment on the open one with the same title"
80
+ env:
81
+ LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
82
+ LINEAR_TEAM_ID: ${{ secrets.LINEAR_TEAM_TELEMETRY_ID }}
83
+ LINEAR_STATUS_PLAN_ID: ${{ secrets.LINEAR_STATUS_PLAN_ID }}
84
+ LINEAR_PROJECT_INGESTION_FOR_EVERYTHING_ID: ${{ secrets.LINEAR_PROJECT_INGESTION_FOR_EVERYTHING_ID }}
85
+ LINEAR_ASSIGNEE_PETR_ID: ${{ secrets.LINEAR_ASSIGNEE_PETR_ID }}
86
+ CANARY: ${{ steps.ctx.outputs.canary }}
87
+ REPOSITORY: ${{ github.repository }}
88
+ WORKFLOW: ${{ github.event.workflow_run.name || github.workflow }}
89
+ REF_NAME: ${{ github.event.workflow_run.head_branch || github.ref_name }}
90
+ RUN_URL: ${{ github.event.workflow_run.html_url || format('{0}/{1}/actions/runs/{2}', github.server_url, github.repository, github.run_id) }}
91
+ RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt || github.run_attempt }}
92
+ EVENT_NAME: ${{ github.event.workflow_run.event || github.event_name }}
93
+ SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
94
+ COMMIT_MESSAGE: ${{ github.event.workflow_run.head_commit.message || github.event.head_commit.message }}
95
+ run: |
96
+ if [ "$CANARY" = true ]; then
97
+ TITLE="E2E canary failure: $WORKFLOW ($REPOSITORY)"; PRIORITY=4
98
+ else
99
+ TITLE="E2E failure: $WORKFLOW ($REPOSITORY)"; PRIORITY=2
100
+ fi
101
+
102
+ # head_commit is only populated on push events; skip the subject otherwise
103
+ COMMIT_LINE="**Commit:** \`${SHA:0:7}\`"
104
+ [ -z "$COMMIT_MESSAGE" ] || COMMIT_LINE="$COMMIT_LINE: $(printf '%s' "$COMMIT_MESSAGE" | head -n1)"
105
+ ATTEMPT_NOTE=""
106
+ [ "$RUN_ATTEMPT" = 1 ] || ATTEMPT_NOTE=" (attempt #$RUN_ATTEMPT)"
107
+ {
108
+ echo "**$WORKFLOW** failed on \`$REF_NAME\` via \`$EVENT_NAME\`$ATTEMPT_NOTE"
109
+ echo
110
+ echo "**Failing jobs:**"
111
+ echo
112
+ cat failed_jobs.md
113
+ echo
114
+ echo "$COMMIT_LINE"
115
+ echo
116
+ echo "[View failing run]($RUN_URL)"
117
+ } > description.md
118
+
119
+ linear() {
120
+ curl -sS --fail-with-body -X POST https://api.linear.app/graphql \
121
+ -H "Authorization: $LINEAR_API_KEY" -H 'Content-Type: application/json' -d "$1"
122
+ }
123
+
124
+ # Open means any state but done, canceled or marked as a duplicate. Better a second issue than none: when the
125
+ # lookup fails, a new issue is created.
126
+ FIND=$(jq -nc --arg title "$TITLE" --arg teamId "$LINEAR_TEAM_ID" '{
127
+ query: "query ($title: String!, $teamId: ID!) { issues(filter: {title: {eq: $title}, team: {id: {eq: $teamId}}, state: {type: {nin: [\"completed\", \"canceled\", \"duplicate\"]}}}, first: 1) { nodes { id url } } }",
128
+ variables: {title: $title, teamId: $teamId}
129
+ }')
130
+ FOUND=$(linear "$FIND") || { echo "::warning::looking for an open issue titled \"$TITLE\" failed: $FOUND"; FOUND='{}'; }
131
+ OPEN=$(jq -r '.data.issues.nodes[0] // empty | "\(.id) \(.url)"' <<<"$FOUND")
132
+
133
+ if [ -n "$OPEN" ]; then
134
+ read -r ISSUE_ID ISSUE_URL <<<"$OPEN"
135
+ PAYLOAD=$(jq -nc --arg issueId "$ISSUE_ID" --rawfile body description.md '{
136
+ query: "mutation ($input: CommentCreateInput!) { commentCreate(input: $input) { success } }",
137
+ variables: {input: {issueId: $issueId, body: $body}}
138
+ }')
139
+ RES=$(linear "$PAYLOAD") || true
140
+ jq -e '.data.commentCreate.success' <<<"$RES" >/dev/null || { echo "::error::commenting on $ISSUE_URL failed: $RES"; exit 1; }
141
+ echo "added this run to the open issue $ISSUE_URL"
142
+ else
143
+ PAYLOAD=$(jq -nc --arg title "$TITLE" --rawfile description description.md --arg teamId "$LINEAR_TEAM_ID" \
144
+ --arg assigneeId "$LINEAR_ASSIGNEE_PETR_ID" --arg stateId "$LINEAR_STATUS_PLAN_ID" \
145
+ --arg projectId "$LINEAR_PROJECT_INGESTION_FOR_EVERYTHING_ID" --argjson priority "$PRIORITY" '{
146
+ query: "mutation ($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { url } } }",
147
+ variables: {input: {title: $title, description: $description, teamId: $teamId, assigneeId: $assigneeId,
148
+ stateId: $stateId, projectId: $projectId, priority: $priority}}
149
+ }')
150
+ RES=$(linear "$PAYLOAD") || true
151
+ ISSUE_URL=$(jq -er '.data.issueCreate.issue.url' <<<"$RES") || { echo "::error::creating the issue failed: $RES"; exit 1; }
152
+ echo "created $ISSUE_URL"
153
+ fi
data/e2e/example.sh ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env bash
2
+ # Installs or runs the example project (example-project/, a Rails app) the way its README tells a customer to, with the
3
+ # leg's Rails, logtail and logtail-rack versions. The output goes to out/<command>.log as well, and a failure ends with
4
+ # one ::error:: line giving the reason.
5
+ # e2e/example.sh install pins the leg's versions in the example's Gemfile and runs bundle install
6
+ # e2e/example.sh run starts bin/rails server, requests /send_logs and /raise_error, and stops the server with
7
+ # Ctrl-C (SIGINT); the logs go to BETTER_STACK_INGESTING_HOST with BETTER_STACK_SOURCE_TOKEN
8
+ # Env: RAILS (a release, or main with RAILS_REF the commit), SQLITE3 (a requirement, or empty for the Gemfile's own),
9
+ # LOGTAIL and LOGTAIL_RACK (releases, or empty for whatever Bundler picks), CLIENT (the logtail-rails release; on
10
+ # pushes and pull requests e2e.yml has installed the gem built from the checkout under that version).
11
+ set -euo pipefail
12
+
13
+ ROOT=$(cd "$(dirname "$0")/.." && pwd)
14
+ OUT=$ROOT/out
15
+ LOG=$OUT/$1.log
16
+ PORT=3000
17
+ mkdir -p "$OUT"
18
+ cd "$ROOT/example-project"
19
+
20
+ # Replaces gem $1's line in the Gemfile, or adds one, with the requirement $2.
21
+ pin() {
22
+ sed -i.orig "/^gem \"$1\"/d" Gemfile && rm Gemfile.orig
23
+ echo "gem \"$1\", $2" >> Gemfile
24
+ }
25
+
26
+ case "$1" in
27
+ install)
28
+ cp Gemfile "$OUT/Gemfile.example"
29
+ if [ "$RAILS" = main ]; then pin rails "github: \"rails/rails\", ref: \"$RAILS_REF\""; else pin rails "\"= $RAILS\""; fi
30
+ if [ -n "$SQLITE3" ]; then pin sqlite3 "\"$SQLITE3\""; fi
31
+ # The example's Gemfile only names logtail-rails, logtail and logtail-rack come with it as its dependencies.
32
+ if [ -n "$LOGTAIL" ]; then pin logtail "\"= $LOGTAIL\""; fi
33
+ if [ -n "$LOGTAIL_RACK" ]; then pin logtail-rack "\"= $LOGTAIL_RACK\""; fi
34
+ pin logtail-rails "\"= $CLIENT\""
35
+ { diff -u "$OUT/Gemfile.example" Gemfile || true; } | tee "$LOG"
36
+ bundle install 2>&1 | tee -a "$LOG" || {
37
+ echo "::error::bundle install failed for Rails $RAILS${RAILS_REF:+ $RAILS_REF} on Ruby $(ruby -e 'print RUBY_VERSION'): $(grep -m 1 -iE 'could not|error|incompatible|requires' "$LOG" || tail -n 1 "$LOG")"
38
+ exit 1
39
+ } ;;
40
+ run)
41
+ bin/rails server -p "$PORT" > "$LOG" 2>&1 &
42
+ SERVER=$!
43
+ # Up when it accepts connections. Polling a page instead would log requests of its own.
44
+ for _ in $(seq 120); do
45
+ if (: > "/dev/tcp/127.0.0.1/$PORT") 2>/dev/null; then break; fi
46
+ kill -0 "$SERVER" 2>/dev/null || { cat "$LOG"; echo "::error::bin/rails server exited before it listened: $(tail -n 1 "$LOG")"; exit 1; }
47
+ sleep 1
48
+ done
49
+ FAILED=""
50
+ SEND_LOGS=$(curl -sS -m 30 -o "$OUT/send_logs.html" -w '%{http_code}' "http://127.0.0.1:$PORT/send_logs") || SEND_LOGS=000
51
+ [ "$SEND_LOGS" = 200 ] || FAILED="GET /send_logs answered HTTP $SEND_LOGS, not 200"
52
+ # The action raises, Rails answers with its error page.
53
+ RAISE_ERROR=$(curl -sS -m 30 -o "$OUT/raise_error.html" -w '%{http_code}' "http://127.0.0.1:$PORT/raise_error") || RAISE_ERROR=000
54
+ [ "$RAISE_ERROR" = 500 ] || FAILED="${FAILED:+$FAILED, }GET /raise_error answered HTTP $RAISE_ERROR, not 500"
55
+ # Ctrl-C, as a customer stops it. Puma stops gracefully and the HTTP log device sends what is still buffered
56
+ # when the process exits.
57
+ kill -INT "$SERVER"
58
+ STATUS=0
59
+ wait "$SERVER" || STATUS=$?
60
+ cat "$LOG"
61
+ [ "$STATUS" = 0 ] || FAILED="${FAILED:+$FAILED, }bin/rails server exited with $STATUS after Ctrl-C"
62
+ # The gem warns on stderr about a blank source token and about batches Better Stack rejected.
63
+ if grep -q '^Logtail: ' "$LOG"; then FAILED="${FAILED:+$FAILED, }$(grep -m 1 '^Logtail: ' "$LOG")"; fi
64
+ if [ -n "$FAILED" ]; then echo "::error::The example project failed: $FAILED"; exit 1; fi ;;
65
+ *) echo "::error::unknown command $1"; exit 1 ;;
66
+ esac
data/e2e/expected.json ADDED
@@ -0,0 +1,109 @@
1
+ [
2
+ {
3
+ "since_rails": "8.1",
4
+ "level": "warn",
5
+ "message_prefix": "Generating image variants require the image_processing gem."
6
+ },
7
+ {
8
+ "level": "debug",
9
+ "message_prefix": " \u001b[1m\u001b[35m (",
10
+ "contains": {"sql_query_executed": {"sql": "CREATE TABLE \"schema_migrations\" (\"version\" varchar NOT NULL PRIMARY KEY)"}}
11
+ },
12
+ {
13
+ "level": "debug",
14
+ "message_prefix": " \u001b[1m\u001b[35m (",
15
+ "contains": {"sql_query_executed": {"sql": "CREATE TABLE \"ar_internal_metadata\" (\"key\" varchar NOT NULL PRIMARY KEY, \"value\" varchar, \"created_at\" datetime(6) NOT NULL, \"updated_at\" datetime(6) NOT NULL)"}}
16
+ },
17
+ {
18
+ "level": "debug",
19
+ "message_prefix": " \u001b[1m\u001b[36mActiveRecord::SchemaMigration Load (",
20
+ "contains": {"sql_query_executed": {"sql": "SELECT \"schema_migrations\".\"version\" FROM \"schema_migrations\" ORDER BY \"schema_migrations\".\"version\" ASC"}}
21
+ },
22
+ {
23
+ "level": "info",
24
+ "message": "Started GET \"/send_logs\"",
25
+ "contains": {"event": {"http_request_received": {"method": "GET", "path": "/send_logs", "host": "127.0.0.1", "port": 3000}}, "context": {"http": {"method": "GET", "path": "/send_logs", "remote_addr": "127.0.0.1"}}}
26
+ },
27
+ {
28
+ "level": "info",
29
+ "message": "Processing by ExampleController#send_logs as */*",
30
+ "contains": {"controller_called": {"controller": "ExampleController", "action": "send_logs"}, "context": {"http": {"path": "/send_logs"}}}
31
+ },
32
+ {
33
+ "level": "debug",
34
+ "message": "Hey! I am debuging!",
35
+ "contains": {"context": {"runtime": {"file": "app/controllers/example_controller.rb", "line": 12, "frame_label": "send_logs"}, "http": {"path": "/send_logs"}}}
36
+ },
37
+ {
38
+ "level": "info",
39
+ "message": "You know what's interesting? Logtail !",
40
+ "contains": {"context": {"runtime": {"file": "app/controllers/example_controller.rb", "line": 15, "frame_label": "send_logs"}, "http": {"path": "/send_logs"}}}
41
+ },
42
+ {
43
+ "level": "warn",
44
+ "message": "Something might not be quite right...",
45
+ "contains": {"context": {"runtime": {"file": "app/controllers/example_controller.rb", "line": 18, "frame_label": "send_logs"}, "http": {"path": "/send_logs"}}}
46
+ },
47
+ {
48
+ "level": "error",
49
+ "message": "Oops! Something went wrong.",
50
+ "contains": {"context": {"runtime": {"file": "app/controllers/example_controller.rb", "line": 21, "frame_label": "send_logs"}, "http": {"path": "/send_logs"}}}
51
+ },
52
+ {
53
+ "level": "fatal",
54
+ "message": "Application crashed! Needs to be fixed ASAP!",
55
+ "contains": {"context": {"runtime": {"file": "app/controllers/example_controller.rb", "line": 24, "frame_label": "send_logs"}, "http": {"path": "/send_logs"}}}
56
+ },
57
+ {
58
+ "level": "info",
59
+ "message": "log message with tags",
60
+ "contains": {"tags": ["tag1", "tag2"], "context": {"runtime": {"file": "app/controllers/example_controller.rb", "line": 28, "frame_label": "send_logs"}, "http": {"path": "/send_logs"}}}
61
+ },
62
+ {
63
+ "level": "warn",
64
+ "message": "log structured data",
65
+ "contains": {"name": {"first": "John", "last": "Smith"}, "id": 123456, "context": {"runtime": {"file": "app/controllers/example_controller.rb", "line": 32, "frame_label": "send_logs"}, "http": {"path": "/send_logs"}}}
66
+ },
67
+ {
68
+ "since_rails": "8.1",
69
+ "level": "info",
70
+ "message_prefix": "[My first event] user_id=123 email=user@example.com at ",
71
+ "contains": {"message": "app/controllers/example_controller.rb:42", "event_name": "My first event", "payload": {"user_id": 123, "email": "user@example.com"}, "source_location": {"filepath": "app/controllers/example_controller.rb", "lineno": 42, "label": "send_logs"}, "context": {"http": {"path": "/send_logs"}}}
72
+ },
73
+ {
74
+ "since_rails": "8.1",
75
+ "level": "info",
76
+ "message_prefix": "[My tagged event with additional context] user_id=123 email=user@example.com at ",
77
+ "contains": {"message": "app/controllers/example_controller.rb:48", "event_name": "My tagged event with additional context", "payload": {"user_id": 123, "email": "user@example.com"}, "tags": ["api"], "source_location": {"filepath": "app/controllers/example_controller.rb", "lineno": 48, "label": "send_logs"}, "context": {"request_id": "abc123", "shop_id": 456, "http": {"path": "/send_logs"}}}
78
+ },
79
+ {
80
+ "level": "info",
81
+ "message_prefix": " Rendered example/send_logs.html.erb within layouts/application (",
82
+ "contains": {"template_rendered": {"name": "example/send_logs.html.erb"}, "context": {"http": {"path": "/send_logs"}}}
83
+ },
84
+ {
85
+ "level": "info",
86
+ "message_prefix": "Completed 200 OK in ",
87
+ "contains": {"event": {"http_response_sent": {"status": 200}}, "context": {"http": {"path": "/send_logs"}}}
88
+ },
89
+ {
90
+ "level": "info",
91
+ "message": "Started GET \"/raise_error\"",
92
+ "contains": {"event": {"http_request_received": {"method": "GET", "path": "/raise_error", "host": "127.0.0.1", "port": 3000}}, "context": {"http": {"method": "GET", "path": "/raise_error", "remote_addr": "127.0.0.1"}}}
93
+ },
94
+ {
95
+ "level": "info",
96
+ "message": "Processing by ExampleController#raise_error as */*",
97
+ "contains": {"controller_called": {"controller": "ExampleController", "action": "raise_error"}, "context": {"http": {"path": "/raise_error"}}}
98
+ },
99
+ {
100
+ "level": "error",
101
+ "message": "ArgumentError (Something went wrong, this action raised an exception!)",
102
+ "contains": {"error": {"name": "ArgumentError", "message": "Something went wrong, this action raised an exception!", "backtrace_json": ["app/controllers/example_controller.rb:55:in "]}, "context": {"http": {"path": "/raise_error"}}}
103
+ },
104
+ {
105
+ "level": "info",
106
+ "message_prefix": "Completed 500 Internal Server Error in ",
107
+ "contains": {"event": {"http_response_sent": {"status": 500}}, "context": {"http": {"path": "/raise_error"}}}
108
+ }
109
+ ]
data/e2e/installed.rb ADDED
@@ -0,0 +1,20 @@
1
+ # Prints the running Ruby and what the example's bundle resolved, as name=value lines for e2e.yml's version check.
2
+ # Run with bundle exec from example-project/.
3
+ rails = Gem.loaded_specs.fetch("rails")
4
+ client = Gem.loaded_specs.fetch("logtail-rails")
5
+ {
6
+ "ruby" => RUBY_VERSION,
7
+ "ruby_description" => RUBY_DESCRIPTION,
8
+ # ruby-head reports the next version with patch level -1
9
+ "ruby_dev" => RUBY_PATCHLEVEL == -1,
10
+ "rails" => rails.version,
11
+ # The commit of a Rails from GitHub, empty for a release
12
+ "rails_ref" => Bundler.locked_gems.specs.find { |spec| spec.name == "rails" }.source.options["revision"],
13
+ "logtail" => Gem.loaded_specs.fetch("logtail").version,
14
+ "logtail_rack" => Gem.loaded_specs.fetch("logtail-rack").version,
15
+ "sqlite3" => Gem.loaded_specs.fetch("sqlite3").version,
16
+ "client" => client.version,
17
+ "client_dir" => client.full_gem_path,
18
+ # The .gem file RubyGems installed it from, kept in its cache
19
+ "client_gem" => client.cache_file,
20
+ }.each { |name, value| puts "#{name}=#{value}" }
data/e2e/plan.sh ADDED
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env bash
2
+ # Plans the E2E matrix: one job per leg, with the Ruby and the Rails, logtail and logtail-rack versions the leg runs the
3
+ # example project (example-project/, a Rails app) with. Prints the matrix as a JSON array, the include list of the test
4
+ # job in e2e.yml.
5
+ # floor the oldest Rails the example runs on (its config.load_defaults) at that line's newest patch, on the oldest
6
+ # Ruby both that Rails release and logtail-rails.gemspec allow, with logtail and logtail-rack at the gemspec's
7
+ # floors
8
+ # rails7 the newest Rails 7 release on Ruby 3.2 with the newest logtail and logtail-rack: the step between the
9
+ # floor and the newest Rails
10
+ # latest the newest stable Ruby that ruby/setup-ruby can install, with the newest Rails, logtail and logtail-rack
11
+ # releases
12
+ # canary ruby-head with the current commit of Rails' main branch, and whatever logtail and logtail-rack Bundler picks
13
+ # Env: LEG (all, floor, rails7, latest or canary), CLIENT_FROM (checkout or rubygems), and RUBY_VERSION_INPUT and
14
+ # RAILS_VERSION_INPUT: an exact Ruby, and an exact Rails release or a commit of Rails' main branch, that replace the
15
+ # planned ones in every leg.
16
+ set -euo pipefail
17
+
18
+ ROOT=$(cd "$(dirname "$0")/.." && pwd)
19
+ RUBYGEMS=https://rubygems.org/api/v1
20
+ # What ruby/setup-ruby@v1 can install. A Ruby released today is in it once setup-ruby supports it.
21
+ SETUP_RUBY_VERSIONS_URL=https://raw.githubusercontent.com/ruby/setup-ruby/v1/ruby-builder-versions.json
22
+ # Job names become artifact names (e2e-summary-<job name>), which must not contain any of these.
23
+ NOT_IN_NAMES='[:"<>|*?/\\]'
24
+
25
+ # A registry hiccup at schedule time should not file a Linear issue, so every lookup is retried.
26
+ get() { curl -fsS -m 60 --retry 3 --retry-delay 10 "$1"; }
27
+ # The newest release of gem $1: rubygems.org's "version" is the highest version that is not a pre-release.
28
+ newest() { get "$RUBYGEMS/gems/$1.json" | jq -er .version; }
29
+ # The X.Y.Z of gem $1's ">= X.Y.Z" in logtail-rails.gemspec.
30
+ gemspec_floor() { sed -n "s/.*add_runtime_dependency \"$1\", .*\">= \([0-9.]*\)\".*/\1/p" "$ROOT/logtail-rails.gemspec"; }
31
+ # The X.Y of a ">= X.Y.Z" requirement.
32
+ minor_of() { sed -n 's/^>= *\([0-9]*\.[0-9]*\).*/\1/p' <<<"$1"; }
33
+ # The higher of two versions.
34
+ higher() { printf '%s\n%s\n' "$1" "$2" | sort -V | tail -n 1; }
35
+
36
+ RAILS_VERSIONS=$(get "$RUBYGEMS/versions/rails.json")
37
+ # The newest release of Rails line $1 (7.0, or 7 for all of Rails 7).
38
+ newest_rails_of() {
39
+ jq -er --arg line "$1." '[.[] | select(.prerelease | not) | .number | select(startswith($line))]
40
+ | sort_by(split(".") | map(tonumber)) | last // empty' <<<"$RAILS_VERSIONS"
41
+ }
42
+
43
+ # The client the example installs: the gem built from the checkout, or the newest release on rubygems.org.
44
+ if [ "${CLIENT_FROM:-checkout}" = rubygems ]; then
45
+ CLIENT=$(newest logtail-rails)
46
+ else
47
+ CLIENT=$(sed -n 's/.*VERSION = "\(.*\)".*/\1/p' "$ROOT/lib/logtail-rails/version.rb")
48
+ fi
49
+
50
+ LEGS=$([ "${LEG:-all}" = all ] && echo "floor rails7 latest canary" || echo "$LEG")
51
+ MATRIX='[]'
52
+ for leg in $LEGS; do
53
+ rails_ref=""
54
+ case "$leg" in
55
+ floor)
56
+ # The example's config.load_defaults names the oldest Rails it boots on: older releases reject that version.
57
+ rails=$(newest_rails_of "$(sed -n 's/^ *config\.load_defaults \([0-9.]*\).*/\1/p' "$ROOT/example-project/config/application.rb")")
58
+ gem_ruby=$(minor_of "$(sed -n "s/.*required_ruby_version = '\(.*\)'.*/\1/p" "$ROOT/logtail-rails.gemspec")")
59
+ rails_ruby=$(minor_of "$(jq -r --arg rails "$rails" '.[] | select(.number == $rails) | .ruby_version' <<<"$RAILS_VERSIONS")")
60
+ ruby=$(higher "$gem_ruby" "$rails_ruby")
61
+ logtail=$(gemspec_floor logtail)
62
+ logtail_rack=$(gemspec_floor logtail-rack) ;;
63
+ rails7)
64
+ ruby=3.2
65
+ rails=$(newest_rails_of 7)
66
+ logtail=$(newest logtail)
67
+ logtail_rack=$(newest logtail-rack) ;;
68
+ latest)
69
+ # The newest X.Y.Z in setup-ruby's list of CRuby builds, which also holds previews, head and debug builds.
70
+ ruby=$(get "$SETUP_RUBY_VERSIONS_URL" | jq -er '.ruby | map(select(test("^[0-9]+\\.[0-9]+\\.[0-9]+$"))) | max_by(split(".") | map(tonumber))')
71
+ rails=$(newest rails)
72
+ logtail=$(newest logtail)
73
+ logtail_rack=$(newest logtail-rack) ;;
74
+ canary)
75
+ ruby=head
76
+ rails=main
77
+ rails_ref=$(git ls-remote https://github.com/rails/rails.git refs/heads/main | cut -f 1)
78
+ logtail=""
79
+ logtail_rack="" ;;
80
+ *) echo "unknown leg $leg" >&2; exit 1 ;;
81
+ esac
82
+ ruby=${RUBY_VERSION_INPUT:-$ruby}
83
+ if [[ "${RAILS_VERSION_INPUT:-}" =~ ^[0-9a-f]{7,40}$ ]]; then
84
+ rails=main
85
+ rails_ref=$RAILS_VERSION_INPUT
86
+ elif [ -n "${RAILS_VERSION_INPUT:-}" ]; then
87
+ rails=$RAILS_VERSION_INPUT
88
+ rails_ref=""
89
+ fi
90
+ for value in "$ruby" "$rails" "$CLIENT"; do
91
+ [ -n "$value" ] || { echo "the $leg leg could not be planned: ruby '$ruby', rails '$rails', client '$CLIENT'" >&2; exit 1; }
92
+ done
93
+ [ "$rails" != main ] || [ -n "$rails_ref" ] || { echo "no commit found on Rails' main branch" >&2; exit 1; }
94
+ # Rails 7.0 and 7.1's sqlite3 adapter requires sqlite3 ~> 1.4, while the example's Gemfile asks for 2.x.
95
+ sqlite3=""
96
+ if [ "$rails" != main ] && [ "$(higher "$rails" 7.2)" != "$rails" ]; then sqlite3="~> 1.4"; fi
97
+
98
+ MATRIX=$(jq -c --arg leg "$leg" --arg ruby "$ruby" --arg rails "$rails" --arg rails_ref "$rails_ref" --arg sqlite3 "$sqlite3" \
99
+ --arg logtail "$logtail" --arg logtail_rack "$logtail_rack" --arg source "${CLIENT_FROM:-checkout}" --arg client "$CLIENT" '. + [{
100
+ name: "\($leg) (Ruby \($ruby), Rails \(if $rails == "main" then "main " + $rails_ref[0:7] else $rails end), \(
101
+ if $logtail == "" then "newest logtail and logtail-rack" else "logtail \($logtail), logtail-rack \($logtail_rack)" end))",
102
+ leg: $leg, ruby: $ruby, rails: $rails, rails_ref: $rails_ref, sqlite3: $sqlite3,
103
+ logtail: $logtail, logtail_rack: $logtail_rack, source: $source, client: $client,
104
+ # The floor and the Rails 7 leg run on the oldest Ubuntu image GitHub offers, the other legs follow ubuntu-latest.
105
+ runner: (if $leg == "floor" or $leg == "rails7" then "ubuntu-24.04" else "ubuntu-latest" end)
106
+ }]' <<<"$MATRIX")
107
+ done
108
+ if jq -e --arg not "$NOT_IN_NAMES" 'any(.[]; .name | test($not))' <<<"$MATRIX" >/dev/null; then
109
+ echo "a job name contains one of $NOT_IN_NAMES, which artifact names do not allow: $(jq -c 'map(.name)' <<<"$MATRIX")" >&2
110
+ exit 1
111
+ fi
112
+ echo "$MATRIX"
@@ -1,10 +1,8 @@
1
1
  source "https://rubygems.org"
2
2
  git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3
3
 
4
- ruby "3.3.4"
5
-
6
4
  # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
7
- gem "rails", "~> 8.1.0.rc1"
5
+ gem "rails", "~> 8.1.0"
8
6
 
9
7
  # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
10
8
  gem "sprockets-rails"
@@ -71,4 +69,4 @@ group :test do
71
69
  gem "webdrivers"
72
70
  end
73
71
 
74
- gem "logtail-rails", "~> 0.2.12"
72
+ gem "logtail-rails", "~> 0.3"
@@ -9,23 +9,23 @@ You can download the example project from GitHub directly or you can clone it to
9
9
  bundle install
10
10
  ```
11
11
 
12
- This will install all dependencies listed in the `Gemfile.lock` file.
12
+ This will install all dependencies listed in the `Gemfile`. The example runs on Rails 7.1 and newer.
13
13
 
14
- Then replace `<SOURCE_TOKEN>` and `<INGESTING_HOST>` in `config/application.rb` with your actual source token and ingesting host which you can find by going to [Sources](https://telemetry.betterstack.com/team/0/sources) -> Configure in Better Stack.
14
+ `config/application.rb` reads your source token and ingesting host from the environment. You can find them by going to [Sources](https://telemetry.betterstack.com/team/0/sources) -> Configure in Better Stack.
15
15
 
16
16
  ```ruby
17
17
  config.logger = Logtail::Logger.create_default_logger(
18
- "<YOUR_ACTUAL_SOURCE_TOKEN>",
19
- ingesting_host: "in.logs.betterstack.com",
18
+ ENV["BETTER_STACK_SOURCE_TOKEN"],
19
+ ingesting_host: ENV["BETTER_STACK_INGESTING_HOST"],
20
20
  )
21
21
  ```
22
22
 
23
23
  ## Run the example project
24
24
 
25
- To run the example application, run the following command:
25
+ To run the example application, run the following command with your source token and ingesting host:
26
26
 
27
27
  ```bash
28
- rails server
28
+ BETTER_STACK_SOURCE_TOKEN=<source_token> BETTER_STACK_INGESTING_HOST=<ingesting_host> bin/rails server
29
29
  ```
30
30
 
31
31
  This will open a local server [127.0.0.1:3000.](http://127.0.0.1:3000/) On the main page, click the "Let's go!" button to generate test logs.
@@ -37,7 +37,9 @@ All done!
37
37
  Log into your Logtail account to check your logs.
38
38
  ```
39
39
 
40
- This will create a total of 6 different logs, each corresponding to a different log level and one with additional structured data. You can review these logs in Logtail.
40
+ This will create a log for each log level, one with tags and one with additional structured data, and on Rails 8.1 and newer two events reported with `Rails.event`, next to the request, controller and template logs of the Rails integration. Click "Raise an exception" on the main page to see how an exception raised in a controller action is logged. You can review these logs in Better Stack.
41
+
42
+ Stop the server with Ctrl-C. The logger sends what it still holds before the server exits.
41
43
 
42
44
  # Logging
43
45
 
@@ -38,13 +38,20 @@ class ExampleController < ApplicationController
38
38
  )
39
39
 
40
40
  # Since Rails 8.1, you can also use the Rails.event to send events with structured data to Better Stack.
41
- Rails.event.notify("My first event", user_id: 123, email: "user@example.com")
42
-
43
- # You can add context to all events
44
- Rails.event.set_context(request_id: "abc123", shop_id: 456)
45
- # And tags specific events
46
- Rails.event.tagged("api") do
47
- Rails.event.notify("My tagged event with additional context", user_id: 123, email: "user@example.com")
41
+ if Rails.respond_to?(:event)
42
+ Rails.event.notify("My first event", user_id: 123, email: "user@example.com")
43
+
44
+ # You can add context to all events
45
+ Rails.event.set_context(request_id: "abc123", shop_id: 456)
46
+ # And tags specific events
47
+ Rails.event.tagged("api") do
48
+ Rails.event.notify("My tagged event with additional context", user_id: 123, email: "user@example.com")
49
+ end
48
50
  end
49
51
  end
52
+
53
+ # Exceptions raised in an action are sent to Better Stack too, with their class, message and backtrace
54
+ def raise_error
55
+ raise ArgumentError, "Something went wrong, this action raised an exception!"
56
+ end
50
57
  end
@@ -1,3 +1,5 @@
1
1
  <h1>Hello from Logtail</h1>
2
2
  <p>Click the button below to send some example logs</p>
3
- <%= button_to "Let's go!", send_logs_path, method: :get %>
3
+ <%= button_to "Let's go!", send_logs_path, method: :get %>
4
+ <p>Or raise an exception in a controller action</p>
5
+ <%= button_to "Raise an exception", raise_error_path, method: :get %>