@@ -61,9 +61,50 @@ public actor DiscordLogManager {
61
61
}
62
62
}
63
63
}
64
-
64
+
65
+ public enum LogAttachmentPolicy : Sendable {
66
+ case disabled
67
+ /// Use ``enabled(formatter:)`` for better type-inference by compiler.
68
+ /// Otherwise you can use this too.
69
+ case enabled( _formatter: any LogFormatter = . json)
70
+
71
+ /// Enable the log attachments.
72
+ public static var enabled : Self {
73
+ . enabled( formatter: . json)
74
+ }
75
+
76
+ /// Enable the log attachments.
77
+ ///
78
+ /// To enable compiler-helps with `LogFormatter`-extension inferences like `.json`.
79
+ /// Uses `some LogFormatter` instead of `any LogFormatter` for this reason.
80
+ ///
81
+ /// https://github.com/apple/swift/issues/68269
82
+ public static func enabled( formatter: some LogFormatter = . json) -> Self {
83
+ . enabled( _formatter: formatter)
84
+ }
85
+
86
+ var formatter : ( any LogFormatter ) ? {
87
+ switch self {
88
+ case . disabled:
89
+ return nil
90
+ case . enabled( let formatter) :
91
+ return formatter
92
+ }
93
+ }
94
+
95
+ var isDisabled : Bool {
96
+ switch self {
97
+ case . disabled:
98
+ return true
99
+ case . enabled:
100
+ return false
101
+ }
102
+ }
103
+ }
104
+
65
105
let frequency : Duration
66
106
let aliveNotice : AliveNotice ?
107
+ let sendFullLogAsAttachment : LogAttachmentPolicy
67
108
let mentions : [ Logger . Level : [ String ] ]
68
109
let colors : [ Logger . Level : DiscordColor ]
69
110
let excludeMetadata : Set < Logger . Level >
@@ -76,6 +117,8 @@ public actor DiscordLogManager {
76
117
/// - frequency: The frequency of the log-sendings. e.g. if its set to 30s, logs will only be sent once-in-30s. Should not be lower than 10s, because of Discord rate-limits.
77
118
/// - aliveNotice: Configuration for sending "I am alive" messages every once in a while. Note that alive notices are delayed until it's been `interval`-time past last message.
78
119
/// e.g. `Logger(label: "Fallback", factory: StreamLogHandler.standardOutput(label:))`
120
+ /// - sendFullLogAsAttachment: Whether or not to send the full log as an attachment.
121
+ /// The normal logs might need to truncate some stuff when sending as embeds, due to Discord limits.
79
122
/// - mentions: ID of users/roles to be mentioned for each log-level.
80
123
/// - colors: Color of the embeds to be used for each log-level.
81
124
/// - excludeMetadata: Excludes all metadata for these log-levels.
@@ -86,6 +129,7 @@ public actor DiscordLogManager {
86
129
public init (
87
130
frequency: Duration = . seconds( 10 ) ,
88
131
aliveNotice: AliveNotice ? = nil ,
132
+ sendFullLogAsAttachment: LogAttachmentPolicy = . disabled,
89
133
mentions: [ Logger . Level : Mention ] = [ : ] ,
90
134
colors: [ Logger . Level : DiscordColor ] = [
91
135
. critical: . purple,
@@ -104,6 +148,7 @@ public actor DiscordLogManager {
104
148
) {
105
149
self . frequency = frequency
106
150
self . aliveNotice = aliveNotice
151
+ self . sendFullLogAsAttachment = sendFullLogAsAttachment
107
152
self . mentions = mentions. mapValues { $0. toMentionStrings ( ) }
108
153
self . colors = colors
109
154
self . excludeMetadata = excludeMetadata
@@ -116,6 +161,7 @@ public actor DiscordLogManager {
116
161
117
162
struct Log : CustomStringConvertible {
118
163
let embed : Embed
164
+ let attachment : LogInfo ?
119
165
let level : Logger . Level ?
120
166
let isFirstAliveNotice : Bool
121
167
@@ -163,13 +209,25 @@ public actor DiscordLogManager {
163
209
self . fallbackLogger = new ?? Logger ( label: " DBM.LogManager " )
164
210
}
165
211
166
- func include( address: WebhookAddress , embed: Embed , level: Logger . Level ) {
167
- self . include ( address: address, embed: embed, level: level, isFirstAliveNotice: false )
212
+ func include(
213
+ address: WebhookAddress ,
214
+ embed: Embed ,
215
+ attachment: LogInfo ? ,
216
+ level: Logger . Level
217
+ ) {
218
+ self . include (
219
+ address: address,
220
+ embed: embed,
221
+ attachment: attachment,
222
+ level: level,
223
+ isFirstAliveNotice: false
224
+ )
168
225
}
169
226
170
227
private func include(
171
228
address: WebhookAddress ,
172
229
embed: Embed ,
230
+ attachment: LogInfo ? ,
173
231
level: Logger . Level ? ,
174
232
isFirstAliveNotice: Bool
175
233
) {
@@ -187,6 +245,7 @@ public actor DiscordLogManager {
187
245
188
246
self . logs [ address] !. append ( . init(
189
247
embed: embed,
248
+ attachment: attachment,
190
249
level: level,
191
250
isFirstAliveNotice: isFirstAliveNotice
192
251
) )
@@ -232,6 +291,7 @@ public actor DiscordLogManager {
232
291
timestamp: Date ( ) ,
233
292
color: config. color
234
293
) ,
294
+ attachment: nil ,
235
295
level: nil ,
236
296
isFirstAliveNotice: isFirstNotice
237
297
)
@@ -295,21 +355,29 @@ public actor DiscordLogManager {
295
355
296
356
await sendLogsToWebhook (
297
357
content: mentions,
298
- embeds : logs. map ( \ . embed) ,
358
+ logs : logs. map ( { ( $0 . embed, $0 . attachment ) } ) ,
299
359
address: address
300
360
)
301
361
302
362
self . setUpAliveNotices ( )
303
363
}
304
-
364
+
305
365
private func sendLogsToWebhook(
306
366
content: String ,
307
- embeds : [ Embed ] ,
367
+ logs : [ ( embed : Embed , attachment : LogInfo ? ) ] ,
308
368
address: WebhookAddress
309
369
) async {
370
+ let attachment = makeAttachmentData ( attachments: logs. map ( \. attachment) )
371
+
310
372
let payload = Payloads . ExecuteWebhook (
311
373
content: content,
312
- embeds: embeds
374
+ embeds: logs. map ( \. embed) ,
375
+ files: attachment. map { ( name, buffer) in
376
+ [ RawFile ( data: buffer, filename: name) ]
377
+ } ,
378
+ attachments: attachment. map { ( name, _) in
379
+ [ . init( index: 0 , filename: name) ]
380
+ }
313
381
)
314
382
do {
315
383
try await self . client. executeWebhookWithResponse (
@@ -323,7 +391,33 @@ public actor DiscordLogManager {
323
391
] )
324
392
}
325
393
}
326
-
394
+
395
+ private func makeAttachmentData( attachments: [ LogInfo ? ] ) -> ( name: String , data: ByteBuffer ) ? {
396
+ if let formatter = self . configuration. sendFullLogAsAttachment. formatter {
397
+ let attachments : [ LogContainer ] = attachments
398
+ . enumerated ( )
399
+ . filter ( { $0. element != nil } )
400
+ . map ( { LogContainer ( number: $0. offset + 1 , info: $0. element!) } )
401
+ if attachments. isEmpty {
402
+ return nil
403
+ } else {
404
+ var buffer = formatter. format ( logs: attachments)
405
+ /// Discord has a limit of 25MB of attachments.
406
+ /// 24MB is already too much for 10 logs, so we just truncate the buffer.
407
+ let mb24 = 24_000_000
408
+ if buffer. readableBytes > mb24 {
409
+ buffer = buffer. getSlice ( at: buffer. readerIndex, length: mb24) ?? ByteBuffer (
410
+ string: " <error-could-not-slice-buffer-please-report-on-github-in-DiscordLogger-repo> "
411
+ )
412
+ }
413
+ let name = formatter. makeFilename ( logs: attachments)
414
+ return ( name, buffer)
415
+ }
416
+ } else {
417
+ return nil
418
+ }
419
+ }
420
+
327
421
private func logWarning(
328
422
_ message: Logger . Message ,
329
423
metadata: Logger . Metadata ? = nil ,
0 commit comments