Skip to content
Merged
Show file tree
Hide file tree
Changes from 66 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
92b2fbe
Define option to enable logs
limbonaut Oct 6, 2025
55953f1
Add intermediary logging API (as log() method for now) on Apple
limbonaut Oct 7, 2025
f484ea5
Enable logs in the project
limbonaut Oct 7, 2025
437ca29
Add structured log call to project (a test)
limbonaut Oct 7, 2025
2df8297
Change arg order in temporary API method
limbonaut Oct 7, 2025
bbf9c52
Add incomplete native implementation
limbonaut Oct 7, 2025
0b3be7a
Add initial Android implementation
limbonaut Oct 7, 2025
c576edb
Add SentrySDK.logger API
limbonaut Oct 7, 2025
e62c253
Add default values
limbonaut Oct 8, 2025
0a5f97d
Update enrich_events.gd
limbonaut Oct 8, 2025
a911727
Add optimized path for log() on Android
limbonaut Oct 8, 2025
678ac95
Add class descriptions for similar-named SentryLogger and
limbonaut Oct 8, 2025
7ea4b60
Introduce SentryLogLevel enum
limbonaut Oct 8, 2025
792402a
Fix attributes on Android
limbonaut Oct 8, 2025
605fa60
Fix logger test
limbonaut Oct 8, 2025
4dc480f
Add logger.trace()
limbonaut Oct 8, 2025
e58ed52
Introduce experimental options
limbonaut Oct 8, 2025
b3eb911
Fix using experimental option
limbonaut Oct 8, 2025
34db858
Processing structure
limbonaut Oct 8, 2025
77fb5e8
Expland SentryLog, add CocoaLog
limbonaut Oct 8, 2025
d109deb
Export SentryLog API
limbonaut Oct 9, 2025
539dbdf
Add Log.add_attributes() and log.remove_attribute() with Cocoa
limbonaut Oct 9, 2025
b778863
Add NativeLog implementation
limbonaut Oct 9, 2025
3d538c4
Move enum to SentryLog
limbonaut Oct 9, 2025
00a202d
Fix cocoa util variant conversion
limbonaut Oct 9, 2025
2a1dbea
Add tests
limbonaut Oct 9, 2025
a49dba5
Register NativeLog class
limbonaut Oct 9, 2025
b6eb22a
Add mostly implemented AndroidLog
limbonaut Oct 10, 2025
5f7957f
add_attributes and remove_attribute on Android
limbonaut Oct 10, 2025
deba6ae
Fix cutting UTF-8 strings
limbonaut Oct 10, 2025
db26f47
Fix get_attribute() on Android
limbonaut Oct 10, 2025
50c6729
Add mobile tests for structured logs
limbonaut Oct 10, 2025
194377b
Forgot mobile test support in main loop
limbonaut Oct 10, 2025
53c5573
Add UTF-8 test
limbonaut Oct 10, 2025
c810112
Undo changes in enrich_events.gd
limbonaut Oct 10, 2025
06e8005
Fixes for NativeLog
limbonaut Oct 10, 2025
61eba8a
Fix memleak with SentrySDK.logger
limbonaut Oct 10, 2025
16f5f74
Improve native log type error message
limbonaut Oct 10, 2025
b887455
Don't print to engine logger within another log operation
limbonaut Oct 10, 2025
0848118
Fixes for NativeLog
limbonaut Oct 10, 2025
92c69d0
Don't discard event, so not to trigger debug printing
limbonaut Oct 10, 2025
4d0452a
logging: Move print.h to logging
limbonaut Oct 11, 2025
cd61aa6
Move print & logger into logging, extract logging state
limbonaut Oct 11, 2025
fd4290d
Log runtime errors
limbonaut Oct 13, 2025
25f7d14
Log more error attributes
limbonaut Oct 13, 2025
d63a1be
Merge branch 'main' into feat/structured-logging
limbonaut Oct 13, 2025
04a3ee7
Merge branch 'main' into feat/structured-logging
limbonaut Oct 21, 2025
fc9790d
Remove string interpolation and attributes from API
limbonaut Oct 21, 2025
7638bb6
Adjust tests
limbonaut Oct 21, 2025
6ca173f
Update docs
limbonaut Oct 21, 2025
3a1222c
Doc corrections
limbonaut Oct 21, 2025
376d119
Use proper sentry.origin
limbonaut Oct 21, 2025
a52b120
Update mobile_tests.gd
limbonaut Oct 22, 2025
b111391
Fix boolean conv on native
limbonaut Oct 22, 2025
d5a696b
Test type with boolean
limbonaut Oct 22, 2025
01c2b73
Revert type check
limbonaut Oct 22, 2025
b4adbc9
Update test_structured_logs.gd
limbonaut Oct 22, 2025
c8d6e6e
Fix XML formatting
limbonaut Oct 22, 2025
441f7e9
Test trace shortcut
limbonaut Oct 22, 2025
36ff595
Update SentryLogger.xml
limbonaut Oct 22, 2025
3c3b31a
Comment in mobile tests
limbonaut Oct 22, 2025
688d03b
Clarify comment
limbonaut Oct 22, 2025
edfa79d
Fix "warning" => "warn" on native
limbonaut Oct 22, 2025
f25971c
Fix String => Variant on Android
limbonaut Oct 22, 2025
ffddc4d
Update CHANGELOG.md
limbonaut Oct 22, 2025
42b007a
Populate sentry.event_id onlly if not empty
limbonaut Oct 22, 2025
84e0fa9
Merge remote-tracking branch 'origin/main' into feat/structured-logging
limbonaut Oct 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Add user feedback API for collecting and sending user feedback to Sentry ([#418](https://github.com/getsentry/sentry-godot/pull/418))
- Access event exception values in `before_send` handler ([#415](https://github.com/getsentry/sentry-godot/pull/415))
- Add support for Structured Logging ([#409](https://github.com/getsentry/sentry-godot/pull/409))

### Improvements

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ import io.sentry.Breadcrumb
import io.sentry.Hint
import io.sentry.ISerializer
import io.sentry.Sentry
import io.sentry.SentryAttributes
import io.sentry.SentryEvent
import io.sentry.SentryLevel
import io.sentry.SentryLogEvent
import io.sentry.SentryLogEventAttributeValue
import io.sentry.SentryLogLevel
import io.sentry.SentryOptions
import io.sentry.android.core.SentryAndroid
import io.sentry.logger.SentryLogParameters
import io.sentry.protocol.Feedback
import io.sentry.protocol.Message
import io.sentry.protocol.SentryException
Expand Down Expand Up @@ -51,6 +56,12 @@ class SentryAndroidGodotPlugin(godot: Godot) : GodotPlugin(godot) {
}
}

private val logsByHandle = object : ThreadLocal<MutableMap<Int, SentryLogEvent>>() {
override fun initialValue(): MutableMap<Int, SentryLogEvent> {
return mutableMapOf()
}
}

private fun getEvent(eventHandle: Int): SentryEvent? {
val event: SentryEvent? = eventsByHandle.get()?.get(eventHandle)
if (event == null) {
Expand All @@ -75,6 +86,14 @@ class SentryAndroidGodotPlugin(godot: Godot) : GodotPlugin(godot) {
return crumb
}

private fun getLog(logHandle: Int): SentryLogEvent? {
var logEvent: SentryLogEvent? = logsByHandle.get()?.get(logHandle)
if (logEvent == null) {
Log.e(TAG, "Internal Error -- SentryLogEvent not found: $logHandle")
}
return logEvent
}

private fun registerEvent(event: SentryEvent): Int {
val eventsMap = eventsByHandle.get() ?: run {
Log.e(TAG, "Internal Error -- eventsByHandle is null")
Expand Down Expand Up @@ -120,6 +139,21 @@ class SentryAndroidGodotPlugin(godot: Godot) : GodotPlugin(godot) {
return handle
}

private fun registerLog(logEvent: SentryLogEvent): Int {
var logsMap = logsByHandle.get() ?: run {
Log.e(TAG, "Internal Error -- logsMap is null")
return 0
}

var handle = Random.nextInt()
while (logsMap.containsKey(handle)) {
handle = Random.nextInt()
}

logsMap[handle] = logEvent
return handle
}

override fun getPluginName(): String {
return "SentryAndroidGodotPlugin"
}
Expand All @@ -134,6 +168,8 @@ class SentryAndroidGodotPlugin(godot: Godot) : GodotPlugin(godot) {
environment: String,
sampleRate: Float,
maxBreadcrumbs: Int,
enableLogs: Boolean,
beforeSendLogHandlerId: Long
) {
Log.v(TAG, "Initializing Sentry Android")
SentryAndroid.init(godot.getActivity()!!.applicationContext) { options ->
Expand All @@ -146,14 +182,24 @@ class SentryAndroidGodotPlugin(godot: Godot) : GodotPlugin(godot) {
options.maxBreadcrumbs = maxBreadcrumbs
options.sdkVersion?.name = "sentry.java.android.godot"
options.nativeSdkName = "sentry.native.android.godot"
options.logs.isEnabled = enableLogs
options.beforeSend =
SentryOptions.BeforeSendCallback { event: SentryEvent, hint: Hint ->
Log.v(TAG, "beforeSend: ${event.eventId} isCrashed: ${event.isCrashed}")
val handle: Int = registerEvent(event)
Callable.call(beforeSendHandlerId, "before_send", handle)
eventsByHandle.get()?.remove(handle) // Returns the event or null if it was discarded.
}
if (beforeSendLogHandlerId != 0L) {
options.logs.beforeSend =
SentryOptions.Logs.BeforeSendLogCallback { logEvent ->
val handle: Int = registerLog(logEvent)
Callable.call(beforeSendLogHandlerId, "before_send_log", handle)
logsByHandle.get()?.remove(handle) // Returns the log or null if it was discarded.
}
}

}
}

@UsedByGodot
Expand Down Expand Up @@ -239,6 +285,20 @@ class SentryAndroidGodotPlugin(godot: Godot) : GodotPlugin(godot) {
Sentry.addBreadcrumb(crumb)
}

@UsedByGodot
fun log(level: Int, body: String, attributes: Dictionary) {
if (attributes.isEmpty()) {
Sentry.logger().log(level.toSentryLogLevel(), body)
} else {
val sentryAttributes = SentryAttributes.fromMap(attributes)
Sentry.logger().log(
level.toSentryLogLevel(),
SentryLogParameters.create(sentryAttributes),
body
)
}
}

@UsedByGodot
fun captureMessage(message: String, level: Int): String {
val id = Sentry.captureMessage(message, level.toSentryLevel())
Expand Down Expand Up @@ -573,4 +633,72 @@ class SentryAndroidGodotPlugin(godot: Godot) : GodotPlugin(godot) {
return crumb.timestamp.toMicros()
}

@UsedByGodot
fun releaseLog(handle: Int) {
val logsMap = logsByHandle.get() ?: run {
Log.e(TAG, "Internal Error -- logsByHandle is null")
return
}

logsMap.remove(handle)
}

@UsedByGodot
fun logSetLevel(handle: Int, level: Int) {
getLog(handle)?.level = level.toSentryLogLevel()
}

@UsedByGodot
fun logGetLevel(handle: Int): Int {
return getLog(handle)?.level?.toInt() ?: SentryLogLevel.INFO.toInt()
}

@UsedByGodot
fun logSetBody(handle: Int, body: String) {
getLog(handle)?.body = body
}

@UsedByGodot
fun logGetBody(handle: Int): String {
return getLog(handle)?.body ?: ""
}

@UsedByGodot
fun logGetAttribute(handle: Int, name: String): Dictionary {
// NOTE: Use Dictionary container for the value to avoid object wrapper creation.
var attr = getLog(handle)?.attributes?.get(name) ?: return Dictionary()
val result = Dictionary()
result["type"] = attr.type
result["value"] = attr.value
return result
}

@UsedByGodot
fun logSetAttribute(handle: Int, name: String, type: String, value: Any) {
val log = getLog(handle) ?: return
val logAttributes = log.attributes ?: HashMap<String, SentryLogEventAttributeValue>().also { log.attributes = it }
logAttributes[name] = SentryLogEventAttributeValue(type, value)
}

@UsedByGodot
fun logAddAttributes(handle: Int, attributes: Dictionary) {
val log = getLog(handle) ?: return
val logAttributes = log.attributes ?: HashMap<String, SentryLogEventAttributeValue>().also { log.attributes = it }
for ((key, value) in attributes) {
val attrValue = when(value) {
is Boolean -> SentryLogEventAttributeValue("boolean", value)
is Int, is Long -> SentryLogEventAttributeValue("integer", value)
is Float, is Double -> SentryLogEventAttributeValue("double", value)
is String -> SentryLogEventAttributeValue("string", value)
else -> SentryLogEventAttributeValue("string", value.toString())
}
logAttributes[key.toString()] = attrValue
}
}

@UsedByGodot
fun logRemoveAttribute(handle: Int, name: String) {
getLog(handle)?.attributes?.remove(name)
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.sentry.godotplugin

import io.sentry.SentryLevel
import io.sentry.SentryLogLevel
import java.util.Date
import java.time.Instant

Expand All @@ -14,6 +15,17 @@ fun Int.toSentryLevel(): SentryLevel =
else -> SentryLevel.ERROR
}

fun Int.toSentryLogLevel(): SentryLogLevel =
when (this) {
0 -> SentryLogLevel.TRACE
1 -> SentryLogLevel.DEBUG
2 -> SentryLogLevel.INFO
3 -> SentryLogLevel.WARN
4 -> SentryLogLevel.ERROR
5 -> SentryLogLevel.FATAL
else -> SentryLogLevel.INFO
}

fun SentryLevel.toInt(): Int =
when (this) {
SentryLevel.DEBUG -> 0
Expand All @@ -23,6 +35,16 @@ fun SentryLevel.toInt(): Int =
SentryLevel.FATAL -> 4
}

fun SentryLogLevel.toInt(): Int =
when (this) {
SentryLogLevel.TRACE -> 0
SentryLogLevel.DEBUG -> 1
SentryLogLevel.INFO -> 2
SentryLogLevel.WARN -> 3
SentryLogLevel.ERROR -> 4
SentryLogLevel.FATAL -> 5
}

fun Long.microsecondsToTimestamp(): Date {
val millis = this / 1_000
return Date(millis)
Expand Down
31 changes: 31 additions & 0 deletions doc_classes/SentryExperimental.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="SentryExperimental" inherits="RefCounted" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/godotengine/godot/master/doc/class.xsd">
<brief_description>
Experimental options for Sentry SDK.
</brief_description>
<description>
Contains configuration options for experimental features of the [SentrySDK]. These features may be unstable or subject to change in future versions.
Access this configuration through [member SentryOptions.experimental].
</description>
<tutorials>
</tutorials>
<members>
<member name="before_send_log" type="Callable" setter="set_before_send_log" getter="get_before_send_log" default="Callable()">
If assigned, this callback will be called before sending a log message to Sentry. It can be used to modify the log message or prevent it from being sent.
[codeblock]
func _before_send_log(log_entry: SentryLog) -&gt; SentryLog:
# Filter junk.
if log_entry.body == "Junk message":
return null
# Remove sensitive information from log messages.
log_entry.body = log_entry.body.replace("Bruno", "REDACTED")
# Add custom attributes.
log_entry.set_attribute("current_scene", current_scene.name)
return log_entry
[/codeblock]
</member>
<member name="enable_logs" type="bool" setter="set_enable_logs" getter="get_enable_logs" default="false">
Enables Sentry structured logging functionality. When enabled, Godot's log messages are automatically captured and sent to Sentry, and you gain access to the dedicated logging APIs available through [member SentrySDK.logger].
</member>
</members>
</class>
73 changes: 73 additions & 0 deletions doc_classes/SentryLog.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="SentryLog" inherits="RefCounted" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/godotengine/godot/master/doc/class.xsd">
<brief_description>
Log entry in Sentry.
</brief_description>
<description>
[SentryLog] represents a single entry in Sentry Logs (aka structured logging).
To learn more about logs in Sentry, visit the [url=https://docs.sentry.io/product/explore/logs/]Sentry Logs[/url] product guide.
</description>
<tutorials>
</tutorials>
<methods>
<method name="add_attributes">
<return type="void" />
<param index="0" name="attributes" type="Dictionary" />
<description>
Adds multiple attributes to the log entry from a Dictionary. Each key-value pair in the dictionary becomes an attribute, where the key serves as the attribute name and the value as the attribute value. This method is most efficient when adding multiple attributes at once. For single attributes, use [method set_attribute] instead.
[b]Note:[/b] Attributes support integers, booleans, floats, and strings. Other data types will be automatically converted to strings.
</description>
</method>
<method name="get_attribute" qualifiers="const">
<return type="Variant" />
<param index="0" name="name" type="String" />
<description>
Retrieves the value of an attribute by its name. If the attribute does not exist, returns [code]null[/code].
</description>
</method>
<method name="remove_attribute">
<return type="void" />
<param index="0" name="name" type="String" />
<description>
Removes an attribute from the log entry by its name.
</description>
</method>
<method name="set_attribute">
<return type="void" />
<param index="0" name="name" type="String" />
<param index="1" name="value" type="Variant" />
<description>
Sets the value of an attribute by its name. If the attribute does not exist, it will be created. For adding multiple attributes at once, use [method add_attributes] instead.
[b]Note:[/b] Attributes support integers, booleans, floats, and strings. Other data types will be automatically converted to strings.
</description>
</method>
</methods>
<members>
<member name="body" type="String" setter="set_body" getter="get_body">
The main text content of the log message.
</member>
<member name="level" type="int" setter="set_level" getter="get_level" enum="SentryLog.LogLevel">
The severity level of the log entry. Uses the [enum LogLevel] enumeration to specify how critical the message is, ranging from trace-level debugging information to fatal errors.
</member>
</members>
<constants>
<constant name="LOG_LEVEL_TRACE" value="0" enum="LogLevel">
A fine-grained debugging event. Typically disabled in default configurations.
</constant>
<constant name="LOG_LEVEL_DEBUG" value="1" enum="LogLevel">
A debugging event.
</constant>
<constant name="LOG_LEVEL_INFO" value="2" enum="LogLevel">
An informational event. Indicates that an event happened.
</constant>
<constant name="LOG_LEVEL_WARN" value="3" enum="LogLevel">
A warning event. Not an error but is likely more important than an informational event.
</constant>
<constant name="LOG_LEVEL_ERROR" value="4" enum="LogLevel">
An error event. Something went wrong.
</constant>
<constant name="LOG_LEVEL_FATAL" value="5" enum="LogLevel">
A fatal error such as application or system crash.
</constant>
</constants>
</class>
Loading
Loading