diff --git a/README.md b/README.md index 9d9beba..f91631b 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,7 @@ All adapters are pure Ruby implementations with no external SDK dependencies: - **AppSignal** - Error tracking and deploy markers - **Insight Hub** (formerly Bugsnag) - Error tracking with breadcrumbs - **Telebugs** - Sentry-compatible protocol (perfect for self-hosted alternatives) +- **OpenObserve** - Open-source observability platform (logs, metrics, traces) ## Configuration @@ -254,6 +255,21 @@ Lapsoss.configure do |config| end ``` +### Using OpenObserve + +```ruby +# OpenObserve - open-source observability platform +Lapsoss.configure do |config| + config.use_openobserve( + endpoint: ENV['OPENOBSERVE_ENDPOINT'], # e.g., "http://localhost:5080" + username: ENV['OPENOBSERVE_USERNAME'], + password: ENV['OPENOBSERVE_PASSWORD'], + org: "default", # optional, defaults to "default" + stream: "errors" # optional, defaults to "errors" + ) +end +``` + ### Advanced Configuration ```ruby diff --git a/lib/lapsoss/adapters/concerns/level_mapping.rb b/lib/lapsoss/adapters/concerns/level_mapping.rb index acf923e..4c7a008 100644 --- a/lib/lapsoss/adapters/concerns/level_mapping.rb +++ b/lib/lapsoss/adapters/concerns/level_mapping.rb @@ -44,6 +44,16 @@ module LevelMapping error: "error", fatal: "error", critical: "error" + }.with_indifferent_access, + + openobserve: { + debug: "DEBUG", + info: "INFO", + warning: "WARN", + warn: "WARN", + error: "ERROR", + fatal: "FATAL", + critical: "FATAL" }.with_indifferent_access }.freeze diff --git a/lib/lapsoss/adapters/openobserve_adapter.rb b/lib/lapsoss/adapters/openobserve_adapter.rb new file mode 100644 index 0000000..8a27f43 --- /dev/null +++ b/lib/lapsoss/adapters/openobserve_adapter.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true + +require "active_support/core_ext/object/blank" +require "base64" + +module Lapsoss + module Adapters + # OpenObserve adapter - sends errors as structured JSON logs + # OpenObserve is an observability platform that accepts logs via simple JSON API + # Docs: https://openobserve.ai/docs/ingestion/ + class OpenobserveAdapter < Base + include Concerns::LevelMapping + include Concerns::HttpDelivery + + self.level_mapping_type = :openobserve + + DEFAULT_STREAM = "errors" + DEFAULT_ORG = "default" + + def initialize(name, settings = {}) + super + + @endpoint = settings[:endpoint].presence || ENV["OPENOBSERVE_ENDPOINT"] + @username = settings[:username].presence || ENV["OPENOBSERVE_USERNAME"] + @password = settings[:password].presence || ENV["OPENOBSERVE_PASSWORD"] + @org = settings[:org].presence || ENV["OPENOBSERVE_ORG"] || DEFAULT_ORG + @stream = settings[:stream].presence || ENV["OPENOBSERVE_STREAM"] || DEFAULT_STREAM + + if @endpoint.blank? || @username.blank? || @password.blank? + Lapsoss.configuration.logger&.warn "[Lapsoss::OpenobserveAdapter] Missing endpoint, username or password - adapter disabled" + @enabled = false + return + end + + setup_endpoint + end + + def capture(event) + deliver(event.scrubbed) + end + + def capabilities + super.merge( + breadcrumbs: false, + code_context: true, + data_scrubbing: true + ) + end + + private + + def setup_endpoint + uri = URI.parse(@endpoint) + @api_endpoint = "#{uri.scheme}://#{uri.host}:#{uri.port}" + @api_path = "/api/#{@org}/#{@stream}/_json" + end + + def build_payload(event) + # OpenObserve expects JSON array of log entries + [ build_log_entry(event) ] + end + + def build_log_entry(event) + entry = { + _timestamp: timestamp_microseconds(event.timestamp), + level: map_level(event.level), + logger: "lapsoss", + environment: event.environment.presence || "production", + service: @settings[:service_name].presence || "rails" + } + + case event.type + when :exception + entry.merge!(build_exception_entry(event)) + when :message + entry[:message] = event.message + else + entry[:message] = event.message || "Unknown event" + end + + # Add optional context + entry[:user] = event.user_context if event.user_context.present? + entry[:tags] = event.tags if event.tags.present? + entry[:extra] = event.extra if event.extra.present? + entry[:request] = event.request_context if event.request_context.present? + entry[:transaction] = event.transaction if event.transaction.present? + entry[:fingerprint] = event.fingerprint if event.fingerprint.present? + + entry.compact_blank + end + + def build_exception_entry(event) + entry = { + message: "#{event.exception_type}: #{event.exception_message}", + exception_type: event.exception_type, + exception_message: event.exception_message + } + + if event.has_backtrace? + entry[:stacktrace] = format_stacktrace(event) + entry[:stacktrace_raw] = event.backtrace_frames.map do |frame| + "#{frame.absolute_path || frame.filename}:#{frame.line_number} in `#{frame.method_name}`" + end + end + + entry + end + + def format_stacktrace(event) + event.backtrace_frames.map do |frame| + frame_entry = { + filename: frame.filename, + abs_path: frame.absolute_path || frame.filename, + function: frame.method_name || frame.function, + lineno: frame.line_number, + in_app: frame.in_app + } + + if frame.code_context.present? + frame_entry[:context_line] = frame.code_context[:context_line] + frame_entry[:pre_context] = frame.code_context[:pre_context] + frame_entry[:post_context] = frame.code_context[:post_context] + end + + frame_entry.compact + end + end + + def timestamp_microseconds(time) + # OpenObserve expects _timestamp in microseconds + (time.to_f * 1_000_000).to_i + end + + def serialize_payload(payload) + json = ActiveSupport::JSON.encode(payload) + + if json.bytesize >= compress_threshold + [ ActiveSupport::Gzip.compress(json), true ] + else + [ json, false ] + end + end + + def compress_threshold + @settings[:compress_threshold] || 1024 + end + + def adapter_specific_headers + credentials = Base64.strict_encode64("#{@username}:#{@password}") + { + "Authorization" => "Basic #{credentials}" + } + end + end + end +end diff --git a/lib/lapsoss/configuration.rb b/lib/lapsoss/configuration.rb index 981b825..89653cd 100644 --- a/lib/lapsoss/configuration.rb +++ b/lib/lapsoss/configuration.rb @@ -117,6 +117,11 @@ def use_logger(name: :logger, **settings) register_adapter(name, :logger, **settings) end + # Convenience method for OpenObserve + def use_openobserve(name: :openobserve, **settings) + register_adapter(name, :openobserve, **settings) + end + # Apply configuration by registering all adapters def apply! Registry.instance.clear! diff --git a/test/cassettes/openobserve_capture_exception.yml b/test/cassettes/openobserve_capture_exception.yml new file mode 100644 index 0000000..2123913 --- /dev/null +++ b/test/cassettes/openobserve_capture_exception.yml @@ -0,0 +1,49 @@ +--- +http_interactions: +- request: + method: post + uri: "/api/default/errors/_json" + body: + encoding: ASCII-8BIT + string: !binary |- + H4sIAGoeNmkAA9VX33PaRhD+V67yC+4QCQwYo5k8uIk77UwTZhw3fTCMfJIWuLG409ydSNwk/3v3TgKEQLY6k4bCg5B0u3u733f7Q/dfnECzJShNl6njd4eXg+6g3+92RqOLznDQdhJYQeL4zu/vfx07+Cjmc5D4nNBUCaXwFfAVk4IvgWt8n0oRZ5FmguOSArliEeBrSVlihHEnRefmzQdNeUxlfCOlkD65QxcImHsyk2JJ/tja/xxBagwG+ind09wR2FqvN4eRRo9aUuPW/RdnxhLgdGl0VpH0Ms0Sb0Ulo2ECAZVzFYSJiB6DiCYJSFeGaIKGKkipXqCO9yfGqDwFmRTKc1GUJp5aUAnekinwGMf9kkR5MgufvJ7bd4dewsL8cQ5LZd918lt04NWl23O7VqSxO7OM54D7zsc3t77/sZC/RvFfjPQbK3xmdHJ1QyTjwIXjj9oO4wFNkfwZTRS0nUhwDZ91YCTQJLE/q+YaC62fjRvnaCKVEBTCCGUhSIhZJq/tnztjUunWvb1X7G9oF4bQQ/00dZeMG0O5HvAY750pGhZKVy3nq+t/c51+a1fp+8H0HIL/LFOIClUKtIYSzt1RvxnSW5Za9toma2s1mK+XEXMMB6TebN8yyLSJsMlhKSvADmHOeB3SElSUAalm2dq73LQrMx7MMKkh/mmziEmuYI8XjanoiRRRCE09wGMc01SDDMzCHmMNpEuAW4QwbDLJOp1eFCXonj/eal/nyqYaWIkdRoZbQrTMDvOBjLplRlsTp+xchOYzpGRTgibOOYnFIaKK422wMqGQiVMoK7LRJloQ4/04Nz9xjK0amkhR3l7vMuVy+IRO1tQ/dG+bb1YcowrCdUVs5WXFnpQqjZirzJJzkLbvmmjrrV4N3IuBO7CCB7bfOwetC2LblTo3R+JdoeH7BowzPLDlstcwG81PQTJDnHic31k8aqvfrsJCiMcN4C8XuZLEVunu5vr27fiv98G7m7vfxm8/uECjBR4M8tVY/3paRFVI2csftVMyOxWSTinSTWl69hxeVftvTYjHi+4ZCm8zzs2gcWYmyIDpUly9y6sT5u55yhpG9j+kzHZtwQEHZb0QcTnXLjqnG9b2JNYH2O8PTy6+57taOeZyoL1qa/s2LX/1BJJ+Mv3m6F8u/sjE89Dwk+UBQ/zPp3kzpa+d2hnkze4vTaY4UFrdfz2TfvfQmhQ8v4i02eR0HBdxALA+vjg0HMe9q9Eu30eGzbiEbbeCWLVHHwmqqltHQ8g0uh1fKi3jhzuEjamGsqN71mtSIsrePuDHyPQf8q9KTlMUAAA= + headers: + User-Agent: + - lapsoss/ + Content-Type: + - application/json + Content-Encoding: + - gzip + X-Lapsoss-Version: + - "" + Authorization: + - "" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Transfer-Encoding: + - chunked + Vary: + - Origin, Access-Control-Request-Method, Access-Control-Request-Headers + - accept-encoding + O2-Process-Time: + - '1765154411052337' + Access-Control-Allow-Credentials: + - 'true' + X-Api-Node: + - bbb966dc81dd + Content-Type: + - application/json + Date: + - Mon, 08 Dec 2025 00:40:10 GMT + body: + encoding: ASCII-8BIT + string: '{"code":200,"status":[{"name":"errors","successful":1,"failed":0}]}' + recorded_at: Mon, 08 Dec 2025 00:40:11 GMT +recorded_with: VCR 6.3.1 diff --git a/test/cassettes/openobserve_capture_message.yml b/test/cassettes/openobserve_capture_message.yml new file mode 100644 index 0000000..7152e57 --- /dev/null +++ b/test/cassettes/openobserve_capture_message.yml @@ -0,0 +1,47 @@ +--- +http_interactions: +- request: + method: post + uri: "/api/default/errors/_json" + body: + encoding: UTF-8 + string: '[{"_timestamp":1765154410989036,"level":"INFO","logger":"lapsoss","environment":"production","service":"rails","message":"Info + message from Lapsoss"}]' + headers: + User-Agent: + - lapsoss/ + Content-Type: + - application/json + X-Lapsoss-Version: + - "" + Authorization: + - "" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Transfer-Encoding: + - chunked + Content-Type: + - application/json + Access-Control-Allow-Credentials: + - 'true' + O2-Process-Time: + - '1765154410990831' + X-Api-Node: + - bbb966dc81dd + Vary: + - Origin, Access-Control-Request-Method, Access-Control-Request-Headers + - accept-encoding + Date: + - Mon, 08 Dec 2025 00:40:10 GMT + body: + encoding: ASCII-8BIT + string: '{"code":200,"status":[{"name":"errors","successful":1,"failed":0}]}' + recorded_at: Mon, 08 Dec 2025 00:40:10 GMT +recorded_with: VCR 6.3.1 diff --git a/test/cassettes/openobserve_custom_stream.yml b/test/cassettes/openobserve_custom_stream.yml new file mode 100644 index 0000000..ef123a0 --- /dev/null +++ b/test/cassettes/openobserve_custom_stream.yml @@ -0,0 +1,49 @@ +--- +http_interactions: +- request: + method: post + uri: "/api/default/app_errors/_json" + body: + encoding: ASCII-8BIT + string: !binary |- + H4sIAGoeNmkAA9VXbXPaOBD+K6rzhdxQm9ckeKYzlzbcXGca6CRp7kNgHGEW0MSWPJJMk2v6328l82IMJNxMWwofsGztrnafZ7Ur3X1zAs1iUJrGieNXT0+a1WajUa20arVmvV52IphC5PjOx85fXQdfxXgMEt8jmiihFH4CPmVS8Bi4xu+JFMM01ExwnFIgpywE/Cwpi4wwrqTo2Hy51pQPqRy2pRTSJ/ZBtCBhqrSIidISaGzMP4aQGHuBfkrWFFcElsa3WsM4wwctqXHq7pszYhFwGhuVaSi9VLPIm1LJ6CCCgMqxCgaRCB+CkEYRSFcO0AQdqCCheoI63heMUHkKUimU56IojTw1oRK8mCnwGMf1okh5Mh08eXW34Z56ERtkr2OIlf1WyYbowNsTt+5WrcjO7oxSnsHtO7cfrnz/diZ/juLvjfQHK3xkdDJ1QyPjwIXjt8oO4wFNkPoRjRSUnVBwDY86MBJoktifVXONhdIfxo1jNJFICGbCCOVMkBAzTd7ZhztiUunSnR0r9i+UZ4bQQ/3Ud2PGjaFMD/gQx04fDQuli5az2fnT/Pe/l4v0/WJ6NsF/lCpEhSoFWkMO52qrsRvSS5ZK9r9M5ta2YD6fRswxHJB6sXzJIFMmwu4NS9kM7AGMGd+GtAQVpkCKm2zuXWbalSkPRrilYfhmMYlbXMEaLxpLiycSRGFgqgGm8ZAmGmRgJtYY20E6B7hFCMMmvbRSqYdhhO753aX2eaZ8g7pWYoWRWnPJiJbpZkKQUjdPaann5L3LqkuQVZeec0yGYhNLs9w2QJk4SM9RaZIIqdW8Pgk5Jgg4mVsyhrYQhFssCwsZ/5QVYd+fRYqj9fBdDl9Lfs7t8oIy61oiGNdYgDu3dz2n+7nd6b6/bl/dtoN25+Jz92Pnpuf0yfMzej3ROvE9z+6jCbrmNytnlZ6Tt4dgScP8Jntf8NE5v2wv7GUb8094xN4TgRuK2BgrZhCWCWbzYmPG/NA9Pl/qbdOtNd2mFdyw/FoKlmrE9kl1bLLxcqbh+ybzjnCv5CvujoXA/BREI1eByQszsnhsLbyrChMhHnK0vFZfcxJLpZv2+dVF959OcNm++bt7ce0CDSeYmeTZWH8+LKIKpIS4NVIEcnF4UCvVulIg6ZAiXVTFF/PwrNj6t4S4v+heoPAq5dyccY7M0TVgOhdX/eTsgLl7mbIdI/sNKbMHBsEBj+h6IoYrfbhyuGEtM3F7gI3G6cHF93JXy8ecD7RebG3f+/kLVyDpV9Nv9n5p8lsmnvsdb0v3GOJPv0iYC8LcqZU7hFn9tUOxOcta5f99Hv7hse1S8fxZqLsdnfbjIp4ArI+vnhr2495Za5XvPcNmXMK+W0Cs2KT3BFXRrb0hZDrdii+FnvHLHcLOtIWyvXtW36VE5L29x9tI/z9qpQxczRQAAA== + headers: + User-Agent: + - lapsoss/ + Content-Type: + - application/json + Content-Encoding: + - gzip + X-Lapsoss-Version: + - "" + Authorization: + - "" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Transfer-Encoding: + - chunked + Content-Type: + - application/json + X-Api-Node: + - bbb966dc81dd + Vary: + - Origin, Access-Control-Request-Method, Access-Control-Request-Headers + - accept-encoding + O2-Process-Time: + - '1765154410966491' + Access-Control-Allow-Credentials: + - 'true' + Date: + - Mon, 08 Dec 2025 00:40:10 GMT + body: + encoding: ASCII-8BIT + string: '{"code":200,"status":[{"name":"app_errors","successful":1,"failed":0}]}' + recorded_at: Mon, 08 Dec 2025 00:40:10 GMT +recorded_with: VCR 6.3.1 diff --git a/test/cassettes/openobserve_with_code_context.yml b/test/cassettes/openobserve_with_code_context.yml new file mode 100644 index 0000000..b0fee37 --- /dev/null +++ b/test/cassettes/openobserve_with_code_context.yml @@ -0,0 +1,49 @@ +--- +http_interactions: +- request: + method: post + uri: "/api/default/errors/_json" + body: + encoding: ASCII-8BIT + string: !binary |- + H4sIAGoeNmkAA9VX3XPaOBD/V1TnoeSG2nwmwTP3kEtzc31IMpPmeg8J4wh7AU2E5JFk0lzb//1WsgHjQOKbaUvhAYy0u9r9/fZDvv3iRYbNQBs6S72wfXzUb/d7vXZrcNIZdNpNj8McuBd6Hy7/vPLwr5xMQOF/TlMttcYlEHOmpJiBMLieKplksWFS4JYGNWcx4LKijFthPEnTiV25lBdgpjI5V0qqkGQigTETkJCZWyb3YynfkrFURDAeXjJ+xml+3ucYUntAZJ7SZ5bWBFan1TePSMQPRlHr9u0Xb8w4CDqzNuaxCjLDeDCnitERh4iqiY5GXMYPUUw5B+WrEZqgIx2l1ExRJ/gbMdCBhkxJHfgoSnmgp1RBMGMaAibwPM51oLLRU9D1e/5xwNko/zuBmXZrrfwRHXh35Hf9thOp7c44Ezkhoffp7DoMPxXypyj+h5U+c8IHVidXt0QjWEJ64aDpMRHRFJNjTLmGphdLYeCziawEmiTu49R8a6Hxm3XjEE2kCqJCGKEsBAmx2+R39+OPmdKmceueNfsXmoUh9NA8Df0ZE9ZQrgciwWdviIalNlXL+e7i134PvzWr9P1kejbBf5BpRAVTDYyBEs7tQa8e0iuWGu67SRbWtmC+2EbMMRxQZnl8wyLTJNIVi6OsAHsEEya2Ia1AxxmQj4aKhKpl1S28y037KhPRGIsekjfLTWwCGp7xYrD5BDJFFEa2X2AaJzQ1oCK78YyxGtIlwB1CGDa5y1qtbhzbGg+vVtqnufIN6jqJMiPH7RUhRmWb+UBG/TKjjTuv7NwjM1PEL1mScucdkkRuIqpIb4uVDYXceTH6liHaZNnQiLVHrD2ytGfNbWEKjVpykPi1FukLeEQ/a7ZE9HhVgk4dA41GiybZyDuNS54qs1i+zPG1kcnvWnuLo971/U7f7zvBDcc/S41Gh7gJpw9tllwUGmFoM+IAc7jcCWsWqP1o4GPESST5k8Nja0NcV5hK+bAE/PW+V5JYKd2cn16/v/rnMro4v/nr6v1HH2g8xUQhX631r/tFVIWUoiqiZVHotS7aqpC0T5Euu9WLeXhSHclbQtxddC9QeJ0JYe8eB/bSGTFTiqt7dLLH3L1MWc3IfkHK3CCXAqJ8SJRrrdPa37BWmbg9wF7veO/ie3mqlWMuB9qtjrZvw/KLUKToo503O3+ZCQc2nvuabzH3GOIPv+Dbi/vCqbW7vT39tcsq3jGd7v++pn730Oo0vLCItN7NaTcu4gXA+fjqpWE37p0M1vneMWzWJRy7FcSqM3pHUFXd2hlCdtCt+VIZGT/dIRxMWyjbuWfdOi2i7O09vowM/wPlBCX7hhQAAA== + headers: + User-Agent: + - lapsoss/ + Content-Type: + - application/json + Content-Encoding: + - gzip + X-Lapsoss-Version: + - "" + Authorization: + - "" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Transfer-Encoding: + - chunked + X-Api-Node: + - bbb966dc81dd + Content-Type: + - application/json + O2-Process-Time: + - '1765154410986109' + Access-Control-Allow-Credentials: + - 'true' + Vary: + - Origin, Access-Control-Request-Method, Access-Control-Request-Headers + - accept-encoding + Date: + - Mon, 08 Dec 2025 00:40:10 GMT + body: + encoding: ASCII-8BIT + string: '{"code":200,"status":[{"name":"errors","successful":1,"failed":0}]}' + recorded_at: Mon, 08 Dec 2025 00:40:10 GMT +recorded_with: VCR 6.3.1 diff --git a/test/cassettes/openobserve_with_tags.yml b/test/cassettes/openobserve_with_tags.yml new file mode 100644 index 0000000..82e9da5 --- /dev/null +++ b/test/cassettes/openobserve_with_tags.yml @@ -0,0 +1,49 @@ +--- +http_interactions: +- request: + method: post + uri: "/api/default/errors/_json" + body: + encoding: ASCII-8BIT + string: !binary |- + H4sIAGseNmkAA9VYS3PaSBD+K7PyBW8RCQzYRlU5eBNv7R4SVzne7MFQ8iA1MGUxo5oZ4TgJ/327RzyEAJutyoaFA3pMd6v7+6Yf0v03L7JiAsbySeaFzYvzTrPTbjebjU77stuteylMIfVC78+Pv994eKlGI9B4nfLMKGPwFsip0EpOQFq8n2mV5LEVSuKSAT0VMeBtzUVKwvgkw0d050qPctK51lrpkLkDexJ2zCwfObtfYsjIUGSfsw2NNYGV1U0zGFn8aDUnN+6/eUORguQTkp3GOsitSIMp14IPUoi4HplokKr4MYp5moL29QBN8IGJMm7HqBP8hTGZwECulQl8FOVpYMZcQzARBgIh8XlpagKdD56Dlt/2L4JUDIrLEUyMu9coTtGBN+d+y286kb3dGeayADj0Pr+7DcPPc3nEx/xG0u+c8AnpFOpEnJAglRcipUJGPEOyhzw1UPdiJS18sRFJoEnmfk7NJwu1X8mNUzSRaYjmwgjlXJAxWmZv3cEfCm1s7d6dG/EV6nND6KF97vsTIclQoQcywXOvj4aVsVXLxeriSP/9Wb1K30+mZxv8J7lBVLgxYC2UcG522/shvWKp5v7rbGFtB+aLZcQcwwFtl4+vETJ1plxSOMrmYA9gJOQupDWYOAf2yXKZcJ0ssmvhXWHa17mMhpjEkPyyXMSkNrDBi8ViEqgMURhQ/uM2TnhmQUe0sMHYHtIlwB1CGDbr5Y1GK45TdC+8WWlfFcp3qOskyox0SoRYnW/nAxn1y4zWel7ZOSosERWWnnfKErWNoPm2JowoBNbzYvQpR5TZsmCtChRD0PE+1ieWcMt7HlndQRTadtXtLVurhL6EJ3SzUvnQwVWmOTGMJxosamGtKChuj1QJxCwVjpathP3QFFs86k3HP+v4HSe45fEbO6B2xlxjMqe0GT7MNcKQiD/BrVoueHvmIf0MpEPECSlxZw6PnXVvXWGs1OMS8NfLW0lipXR3fXX7/ubvj9GH67s/bt5/8oHHY9wQ7DtZ/35cRFVImSdBtMwBs1YsGxWSjinSZVF6cR9eVjvvjhAPF90LFN7mUtKIcUKzYiRsKa7W+eURc/cyZXtG9j+kzPVrJQFHYztWSTnXzhrHG9ZqJ+4OsN2+OLr4Xu5q5ZjLgbaqrW3WL7/vRJo/Ub85+DtL2KV4HvZ8WXnAEP/zOZ7m84VTayM8Pf21mRRHSaf7r6fRHx7aPgUv7Ja9fW1yOoyLOAA4H18dGg7j3mV3ne8Dw0YuYdutIFbt0QeCqurWwRCiRrfmS6Vl/HSHsDHtoOzgnrX2KRFlbx/oxcZ9YQuxtwKnNMU+Fo8hflQ5zYdTdL9obmd+w5vR5zrsSiSudIK1VCS41DxrtTsojBUYq6twXxbxdWsi8ok3m/X/AcBG7xWeFAAA + headers: + User-Agent: + - lapsoss/ + Content-Type: + - application/json + Content-Encoding: + - gzip + X-Lapsoss-Version: + - "" + Authorization: + - "" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Transfer-Encoding: + - chunked + Access-Control-Allow-Credentials: + - 'true' + Vary: + - Origin, Access-Control-Request-Method, Access-Control-Request-Headers + - accept-encoding + O2-Process-Time: + - '1765154411063412' + X-Api-Node: + - bbb966dc81dd + Content-Type: + - application/json + Date: + - Mon, 08 Dec 2025 00:40:10 GMT + body: + encoding: ASCII-8BIT + string: '{"code":200,"status":[{"name":"errors","successful":1,"failed":0}]}' + recorded_at: Mon, 08 Dec 2025 00:40:11 GMT +recorded_with: VCR 6.3.1 diff --git a/test/cassettes/openobserve_with_user.yml b/test/cassettes/openobserve_with_user.yml new file mode 100644 index 0000000..c0fefbe --- /dev/null +++ b/test/cassettes/openobserve_with_user.yml @@ -0,0 +1,49 @@ +--- +http_interactions: +- request: + method: post + uri: "/api/default/errors/_json" + body: + encoding: ASCII-8BIT + string: !binary |- + H4sIAGseNmkAA9VYUXPaOBD+Kz7lhdxQGwIE8Ewfcg2d68y1mUlz7QMwjmwL0MTIHkkmzaX577crGzAOTtyZXjl4wLa0u9r9Pu1q7fEj8TRfMqXpMiFuu3/ea/e63Xa71T/rD4dNErEVi4hLPnx6f0XgMZ7PmYTniCYqVgqGmFhxGYslExrGExmHaaB5LGBKMbniAYNhSXmEwrCSonMcuU4FLjySMpauZS7WPdcLKwUtK4iFZt80mv8WsATtefohKSvuzG9tV1uDOIM7LSk6NX4kMx4xQZeoswqkk2oeOSsqOfUj5lE5V54fxcGdF9AoYtKWPpigvvISqheg4/wN1pWjWCpj5dggSiNHLahkzpIr5nAB60WRcmTqPzgdu2v3nYj72eOcLZUZa2W34MCbc7tjt41IbXdmqcjgdsmXd9eu+yWXvwDxP1D6nRE+QZ1MHWnkgomYuEAwFx5NgPoZjRRrkhwqDyXApGV+Rs1GC43f0Y1TMJFI5q1xdce5oGXhtPXWXOwZl0o3xuZe8X9YMzcEHuqHqb3kAg1lekyEcE+mYDhWumw5m11f8X/61CzT94vp2Qf/CWw3YEcppjUr4NwedushvWWpYf6b1tpaBebracAcwmFSb5ZvIDJNKzbJYSjLwfbZnIsqpCVTQcqsz5qKkMpwnWVr7zLTtkyFN4OUZuFvm0lIccWe8aKhtDhxAij4WA1gG4c00Ux6OPGMsRrSBcANQhC2NUlbrU4QROCee7XVvsiUb0DXSBQZ6Qy2hGiZ7ucDGLWLjDYmpOgcFhgPC8yEnFphvI+gfFsjRhiCNSEB+JQCytamcD0vVBOC5ioYAqOmvL21ipXQFuwe3KuofODgNtOMOMTj+eta2MgKitkjZQIhS7mhZS9hPzXF1ku96dlnPbtnBPcs/2wHNM4sc0ypU9wMH3MN10XiT2CrFgtezTzEn2LRDHASYXZn8Kise7sKizi+2wD+enkrSGyVbkYX15dXXz95H0c3f15dfrYZDRawL6zvaP37cRFVIiVPAm+TA2qnWLZKJB1TpJui9OI+HJRP3ooQDxfdCxRC3RHYYpxg9fG4LpbV88ERc/cyZTUj+x9SZs7rWDBokfUiDou5dtY63rC2O7E6wG63f3TxvXyqFWMuBtopH21P0+L7jifpPZ43B39ncYcYz23Nl5VbCPE/7+OxP187tdPC4+qv9aTQShrdH+5Gf3podQqem0dar3M6jIvQABgfX20aDuPeYLjL94FhM1vwfFBCrHxGHwiqslsHQwgPuh1fSkfGL3cIDqYKyg7uWadOiSh6e4svNvi+SdxHwkNsKzrZQOErgJlvErakHD8ojt9/+OtmdD26nJKnp+m/2QrWTYgUAAA= + headers: + User-Agent: + - lapsoss/ + Content-Type: + - application/json + Content-Encoding: + - gzip + X-Lapsoss-Version: + - "" + Authorization: + - "" + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Transfer-Encoding: + - chunked + Vary: + - Origin, Access-Control-Request-Method, Access-Control-Request-Headers + - accept-encoding + Access-Control-Allow-Credentials: + - 'true' + X-Api-Node: + - bbb966dc81dd + Content-Type: + - application/json + O2-Process-Time: + - '1765154411078732' + Date: + - Mon, 08 Dec 2025 00:40:10 GMT + body: + encoding: ASCII-8BIT + string: '{"code":200,"status":[{"name":"errors","successful":1,"failed":0}]}' + recorded_at: Mon, 08 Dec 2025 00:40:11 GMT +recorded_with: VCR 6.3.1 diff --git a/test/openobserve_adapter_test.rb b/test/openobserve_adapter_test.rb new file mode 100644 index 0000000..2abb163 --- /dev/null +++ b/test/openobserve_adapter_test.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class OpenobserveAdapterTest < ActiveSupport::TestCase + setup do + @adapter = Lapsoss::Adapters::OpenobserveAdapter.new(:openobserve, + endpoint: ENV["OPENOBSERVE_ENDPOINT"] || "http://localhost:5080", + username: ENV["OPENOBSERVE_USERNAME"] || "seuros@example.com", + password: ENV["OPENOBSERVE_PASSWORD"] || "ShipItFast!", + org: "default", + stream: "errors" + ) + end + + test "captures exception to OpenObserve" do + VCR.use_cassette("openobserve_capture_exception") do + error = StandardError.new("Test error from Lapsoss") + error.set_backtrace(caller) + + event = Lapsoss::Event.build(type: :exception, exception: error) + + response = @adapter.capture(event) + assert response + end + end + + test "captures message to OpenObserve" do + VCR.use_cassette("openobserve_capture_message") do + event = Lapsoss::Event.build(type: :message, message: "Info message from Lapsoss", level: :info) + + response = @adapter.capture(event) + assert response + end + end + + test "captures exception with user context" do + VCR.use_cassette("openobserve_with_user") do + error = RuntimeError.new("Error with user context") + error.set_backtrace(caller) + + event = Lapsoss::Event.build(type: :exception, + exception: error, + context: { + user: { id: 123, username: "testuser", email: "test@example.com" } + }) + + response = @adapter.capture(event) + assert response + end + end + + test "captures exception with tags and extra data" do + VCR.use_cassette("openobserve_with_tags") do + error = ArgumentError.new("Error with tags") + error.set_backtrace(caller) + + event = Lapsoss::Event.build(type: :exception, + exception: error, + context: { + tags: { feature: "checkout", version: "2.0" }, + extra: { order_id: "12345", user_tier: "premium" } + }) + + response = @adapter.capture(event) + assert response + end + end + + test "captures exception with code context" do + VCR.use_cassette("openobserve_with_code_context") do + error = NoMethodError.new("undefined method `foo' for nil:NilClass") + error.set_backtrace(caller) + + event = Lapsoss::Event.build(type: :exception, exception: error) + + response = @adapter.capture(event) + assert response + end + end + + test "disabled when missing credentials" do + adapter = Lapsoss::Adapters::OpenobserveAdapter.new(:openobserve, {}) + + refute adapter.enabled? + end + + test "disabled when missing endpoint" do + adapter = Lapsoss::Adapters::OpenobserveAdapter.new(:openobserve, + username: "test", + password: "test" + ) + + refute adapter.enabled? + end + + test "uses environment variables when settings not provided" do + with_env("OPENOBSERVE_ENDPOINT", "http://test.example.com:5080") do + with_env("OPENOBSERVE_USERNAME", "envuser@example.com") do + with_env("OPENOBSERVE_PASSWORD", "EnvPass123") do + adapter = Lapsoss::Adapters::OpenobserveAdapter.new(:openobserve, {}) + + assert adapter.enabled? + end + end + end + end + + test "configuration helper registers adapter" do + Lapsoss.configure do |config| + config.use_openobserve( + endpoint: "http://localhost:5080", + username: "test@example.com", + password: "TestPass123" + ) + end + Lapsoss.configuration.apply! + + adapter = Lapsoss::Registry.instance[:openobserve] + assert_not_nil adapter + assert_kind_of Lapsoss::Adapters::OpenobserveAdapter, adapter + end + + test "supports custom org and stream" do + VCR.use_cassette("openobserve_custom_stream") do + adapter = Lapsoss::Adapters::OpenobserveAdapter.new(:openobserve, + endpoint: ENV["OPENOBSERVE_ENDPOINT"] || "http://localhost:5080", + username: ENV["OPENOBSERVE_USERNAME"] || "seuros@example.com", + password: ENV["OPENOBSERVE_PASSWORD"] || "ShipItFast!", + org: "default", + stream: "app_errors" + ) + + error = StandardError.new("Error to custom stream") + error.set_backtrace(caller) + + event = Lapsoss::Event.build(type: :exception, exception: error) + + response = adapter.capture(event) + assert response + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 1db7d62..1282138 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -27,6 +27,9 @@ config.filter_sensitive_data("") { ENV["INSIGHT_HUB_API_KEY"] || "test-api-key" } config.filter_sensitive_data("") { ENV["BUGSNAG_API_KEY"] || "test-api-key" } config.filter_sensitive_data("") { ENV["ROLLBAR_ACCESS_TOKEN"] || "test-token" } + config.filter_sensitive_data("") { ENV["OPENOBSERVE_ENDPOINT"] || "http://localhost:5080" } + config.filter_sensitive_data("") { ENV["OPENOBSERVE_USERNAME"] || "seuros@example.com" } + config.filter_sensitive_data("") { ENV["OPENOBSERVE_PASSWORD"] || "ShipItFast!" } config.filter_sensitive_data("") { ENV["AUTHORIZATION"] } config.filter_sensitive_data("") { ENV["HTTP_COOKIE"] }