Skip to content

Commit 624b9c2

Browse files
authored
feat: Structured logging (#409)
1 parent b25f290 commit 624b9c2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1614
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

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

1011
### Improvements
1112

android_lib/src/main/java/io/sentry/godotplugin/SentryAndroidGodotPlugin.kt

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@ import io.sentry.Breadcrumb
66
import io.sentry.Hint
77
import io.sentry.ISerializer
88
import io.sentry.Sentry
9+
import io.sentry.SentryAttributes
910
import io.sentry.SentryEvent
1011
import io.sentry.SentryLevel
12+
import io.sentry.SentryLogEvent
13+
import io.sentry.SentryLogEventAttributeValue
14+
import io.sentry.SentryLogLevel
1115
import io.sentry.SentryOptions
1216
import io.sentry.android.core.SentryAndroid
17+
import io.sentry.logger.SentryLogParameters
1318
import io.sentry.protocol.Feedback
1419
import io.sentry.protocol.Message
1520
import io.sentry.protocol.SentryException
@@ -51,6 +56,12 @@ class SentryAndroidGodotPlugin(godot: Godot) : GodotPlugin(godot) {
5156
}
5257
}
5358

59+
private val logsByHandle = object : ThreadLocal<MutableMap<Int, SentryLogEvent>>() {
60+
override fun initialValue(): MutableMap<Int, SentryLogEvent> {
61+
return mutableMapOf()
62+
}
63+
}
64+
5465
private fun getEvent(eventHandle: Int): SentryEvent? {
5566
val event: SentryEvent? = eventsByHandle.get()?.get(eventHandle)
5667
if (event == null) {
@@ -75,6 +86,14 @@ class SentryAndroidGodotPlugin(godot: Godot) : GodotPlugin(godot) {
7586
return crumb
7687
}
7788

89+
private fun getLog(logHandle: Int): SentryLogEvent? {
90+
var logEvent: SentryLogEvent? = logsByHandle.get()?.get(logHandle)
91+
if (logEvent == null) {
92+
Log.e(TAG, "Internal Error -- SentryLogEvent not found: $logHandle")
93+
}
94+
return logEvent
95+
}
96+
7897
private fun registerEvent(event: SentryEvent): Int {
7998
val eventsMap = eventsByHandle.get() ?: run {
8099
Log.e(TAG, "Internal Error -- eventsByHandle is null")
@@ -120,6 +139,21 @@ class SentryAndroidGodotPlugin(godot: Godot) : GodotPlugin(godot) {
120139
return handle
121140
}
122141

142+
private fun registerLog(logEvent: SentryLogEvent): Int {
143+
var logsMap = logsByHandle.get() ?: run {
144+
Log.e(TAG, "Internal Error -- logsMap is null")
145+
return 0
146+
}
147+
148+
var handle = Random.nextInt()
149+
while (logsMap.containsKey(handle)) {
150+
handle = Random.nextInt()
151+
}
152+
153+
logsMap[handle] = logEvent
154+
return handle
155+
}
156+
123157
override fun getPluginName(): String {
124158
return "SentryAndroidGodotPlugin"
125159
}
@@ -134,6 +168,8 @@ class SentryAndroidGodotPlugin(godot: Godot) : GodotPlugin(godot) {
134168
environment: String,
135169
sampleRate: Float,
136170
maxBreadcrumbs: Int,
171+
enableLogs: Boolean,
172+
beforeSendLogHandlerId: Long
137173
) {
138174
Log.v(TAG, "Initializing Sentry Android")
139175
SentryAndroid.init(godot.getActivity()!!.applicationContext) { options ->
@@ -146,14 +182,24 @@ class SentryAndroidGodotPlugin(godot: Godot) : GodotPlugin(godot) {
146182
options.maxBreadcrumbs = maxBreadcrumbs
147183
options.sdkVersion?.name = "sentry.java.android.godot"
148184
options.nativeSdkName = "sentry.native.android.godot"
185+
options.logs.isEnabled = enableLogs
149186
options.beforeSend =
150187
SentryOptions.BeforeSendCallback { event: SentryEvent, hint: Hint ->
151188
Log.v(TAG, "beforeSend: ${event.eventId} isCrashed: ${event.isCrashed}")
152189
val handle: Int = registerEvent(event)
153190
Callable.call(beforeSendHandlerId, "before_send", handle)
154191
eventsByHandle.get()?.remove(handle) // Returns the event or null if it was discarded.
155192
}
193+
if (beforeSendLogHandlerId != 0L) {
194+
options.logs.beforeSend =
195+
SentryOptions.Logs.BeforeSendLogCallback { logEvent ->
196+
val handle: Int = registerLog(logEvent)
197+
Callable.call(beforeSendLogHandlerId, "before_send_log", handle)
198+
logsByHandle.get()?.remove(handle) // Returns the log or null if it was discarded.
199+
}
156200
}
201+
202+
}
157203
}
158204

159205
@UsedByGodot
@@ -239,6 +285,20 @@ class SentryAndroidGodotPlugin(godot: Godot) : GodotPlugin(godot) {
239285
Sentry.addBreadcrumb(crumb)
240286
}
241287

288+
@UsedByGodot
289+
fun log(level: Int, body: String, attributes: Dictionary) {
290+
if (attributes.isEmpty()) {
291+
Sentry.logger().log(level.toSentryLogLevel(), body)
292+
} else {
293+
val sentryAttributes = SentryAttributes.fromMap(attributes)
294+
Sentry.logger().log(
295+
level.toSentryLogLevel(),
296+
SentryLogParameters.create(sentryAttributes),
297+
body
298+
)
299+
}
300+
}
301+
242302
@UsedByGodot
243303
fun captureMessage(message: String, level: Int): String {
244304
val id = Sentry.captureMessage(message, level.toSentryLevel())
@@ -573,4 +633,72 @@ class SentryAndroidGodotPlugin(godot: Godot) : GodotPlugin(godot) {
573633
return crumb.timestamp.toMicros()
574634
}
575635

636+
@UsedByGodot
637+
fun releaseLog(handle: Int) {
638+
val logsMap = logsByHandle.get() ?: run {
639+
Log.e(TAG, "Internal Error -- logsByHandle is null")
640+
return
641+
}
642+
643+
logsMap.remove(handle)
644+
}
645+
646+
@UsedByGodot
647+
fun logSetLevel(handle: Int, level: Int) {
648+
getLog(handle)?.level = level.toSentryLogLevel()
649+
}
650+
651+
@UsedByGodot
652+
fun logGetLevel(handle: Int): Int {
653+
return getLog(handle)?.level?.toInt() ?: SentryLogLevel.INFO.toInt()
654+
}
655+
656+
@UsedByGodot
657+
fun logSetBody(handle: Int, body: String) {
658+
getLog(handle)?.body = body
659+
}
660+
661+
@UsedByGodot
662+
fun logGetBody(handle: Int): String {
663+
return getLog(handle)?.body ?: ""
664+
}
665+
666+
@UsedByGodot
667+
fun logGetAttribute(handle: Int, name: String): Dictionary {
668+
// NOTE: Use Dictionary container for the value to avoid object wrapper creation.
669+
var attr = getLog(handle)?.attributes?.get(name) ?: return Dictionary()
670+
val result = Dictionary()
671+
result["type"] = attr.type
672+
result["value"] = attr.value
673+
return result
674+
}
675+
676+
@UsedByGodot
677+
fun logSetAttribute(handle: Int, name: String, type: String, value: Any) {
678+
val log = getLog(handle) ?: return
679+
val logAttributes = log.attributes ?: HashMap<String, SentryLogEventAttributeValue>().also { log.attributes = it }
680+
logAttributes[name] = SentryLogEventAttributeValue(type, value)
681+
}
682+
683+
@UsedByGodot
684+
fun logAddAttributes(handle: Int, attributes: Dictionary) {
685+
val log = getLog(handle) ?: return
686+
val logAttributes = log.attributes ?: HashMap<String, SentryLogEventAttributeValue>().also { log.attributes = it }
687+
for ((key, value) in attributes) {
688+
val attrValue = when(value) {
689+
is Boolean -> SentryLogEventAttributeValue("boolean", value)
690+
is Int, is Long -> SentryLogEventAttributeValue("integer", value)
691+
is Float, is Double -> SentryLogEventAttributeValue("double", value)
692+
is String -> SentryLogEventAttributeValue("string", value)
693+
else -> SentryLogEventAttributeValue("string", value.toString())
694+
}
695+
logAttributes[key.toString()] = attrValue
696+
}
697+
}
698+
699+
@UsedByGodot
700+
fun logRemoveAttribute(handle: Int, name: String) {
701+
getLog(handle)?.attributes?.remove(name)
702+
}
703+
576704
}

android_lib/src/main/java/io/sentry/godotplugin/UtilityFunctions.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.sentry.godotplugin
22

33
import io.sentry.SentryLevel
4+
import io.sentry.SentryLogLevel
45
import java.util.Date
56
import java.time.Instant
67

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

18+
fun Int.toSentryLogLevel(): SentryLogLevel =
19+
when (this) {
20+
0 -> SentryLogLevel.TRACE
21+
1 -> SentryLogLevel.DEBUG
22+
2 -> SentryLogLevel.INFO
23+
3 -> SentryLogLevel.WARN
24+
4 -> SentryLogLevel.ERROR
25+
5 -> SentryLogLevel.FATAL
26+
else -> SentryLogLevel.INFO
27+
}
28+
1729
fun SentryLevel.toInt(): Int =
1830
when (this) {
1931
SentryLevel.DEBUG -> 0
@@ -23,6 +35,16 @@ fun SentryLevel.toInt(): Int =
2335
SentryLevel.FATAL -> 4
2436
}
2537

38+
fun SentryLogLevel.toInt(): Int =
39+
when (this) {
40+
SentryLogLevel.TRACE -> 0
41+
SentryLogLevel.DEBUG -> 1
42+
SentryLogLevel.INFO -> 2
43+
SentryLogLevel.WARN -> 3
44+
SentryLogLevel.ERROR -> 4
45+
SentryLogLevel.FATAL -> 5
46+
}
47+
2648
fun Long.microsecondsToTimestamp(): Date {
2749
val millis = this / 1_000
2850
return Date(millis)

doc_classes/SentryExperimental.xml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<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">
3+
<brief_description>
4+
Experimental options for Sentry SDK.
5+
</brief_description>
6+
<description>
7+
Contains configuration options for experimental features of the [SentrySDK]. These features may be unstable or subject to change in future versions.
8+
Access this configuration through [member SentryOptions.experimental].
9+
</description>
10+
<tutorials>
11+
</tutorials>
12+
<members>
13+
<member name="before_send_log" type="Callable" setter="set_before_send_log" getter="get_before_send_log" default="Callable()">
14+
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.
15+
[codeblock]
16+
func _before_send_log(log_entry: SentryLog) -&gt; SentryLog:
17+
# Filter junk.
18+
if log_entry.body == "Junk message":
19+
return null
20+
# Remove sensitive information from log messages.
21+
log_entry.body = log_entry.body.replace("Bruno", "REDACTED")
22+
# Add custom attributes.
23+
log_entry.set_attribute("current_scene", current_scene.name)
24+
return log_entry
25+
[/codeblock]
26+
</member>
27+
<member name="enable_logs" type="bool" setter="set_enable_logs" getter="get_enable_logs" default="false">
28+
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].
29+
</member>
30+
</members>
31+
</class>

doc_classes/SentryLog.xml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<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">
3+
<brief_description>
4+
Log entry in Sentry.
5+
</brief_description>
6+
<description>
7+
[SentryLog] represents a single entry in Sentry Logs (aka structured logging).
8+
To learn more about logs in Sentry, visit the [url=https://docs.sentry.io/product/explore/logs/]Sentry Logs[/url] product guide.
9+
</description>
10+
<tutorials>
11+
</tutorials>
12+
<methods>
13+
<method name="add_attributes">
14+
<return type="void" />
15+
<param index="0" name="attributes" type="Dictionary" />
16+
<description>
17+
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.
18+
[b]Note:[/b] Attributes support integers, booleans, floats, and strings. Other data types will be automatically converted to strings.
19+
</description>
20+
</method>
21+
<method name="get_attribute" qualifiers="const">
22+
<return type="Variant" />
23+
<param index="0" name="name" type="String" />
24+
<description>
25+
Retrieves the value of an attribute by its name. If the attribute does not exist, returns [code]null[/code].
26+
</description>
27+
</method>
28+
<method name="remove_attribute">
29+
<return type="void" />
30+
<param index="0" name="name" type="String" />
31+
<description>
32+
Removes an attribute from the log entry by its name.
33+
</description>
34+
</method>
35+
<method name="set_attribute">
36+
<return type="void" />
37+
<param index="0" name="name" type="String" />
38+
<param index="1" name="value" type="Variant" />
39+
<description>
40+
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.
41+
[b]Note:[/b] Attributes support integers, booleans, floats, and strings. Other data types will be automatically converted to strings.
42+
</description>
43+
</method>
44+
</methods>
45+
<members>
46+
<member name="body" type="String" setter="set_body" getter="get_body">
47+
The main text content of the log message.
48+
</member>
49+
<member name="level" type="int" setter="set_level" getter="get_level" enum="SentryLog.LogLevel">
50+
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.
51+
</member>
52+
</members>
53+
<constants>
54+
<constant name="LOG_LEVEL_TRACE" value="0" enum="LogLevel">
55+
A fine-grained debugging event. Typically disabled in default configurations.
56+
</constant>
57+
<constant name="LOG_LEVEL_DEBUG" value="1" enum="LogLevel">
58+
A debugging event.
59+
</constant>
60+
<constant name="LOG_LEVEL_INFO" value="2" enum="LogLevel">
61+
An informational event. Indicates that an event happened.
62+
</constant>
63+
<constant name="LOG_LEVEL_WARN" value="3" enum="LogLevel">
64+
A warning event. Not an error but is likely more important than an informational event.
65+
</constant>
66+
<constant name="LOG_LEVEL_ERROR" value="4" enum="LogLevel">
67+
An error event. Something went wrong.
68+
</constant>
69+
<constant name="LOG_LEVEL_FATAL" value="5" enum="LogLevel">
70+
A fatal error such as application or system crash.
71+
</constant>
72+
</constants>
73+
</class>

0 commit comments

Comments
 (0)