Skip to content

Commit 0d223cf

Browse files
committed
Add OpenTelemetry tracing to EditorServiceManager and formatter
Extended EditorServiceManager and related classes with OpenTelemetry spans to track performance and debug issues effectively. Applied spans to key methods, such as service initialization, formatting, and cache priming, to capture execution details and errors comprehensively.
1 parent 04c5787 commit 0d223cf

File tree

4 files changed

+795
-209
lines changed

4 files changed

+795
-209
lines changed

src/main/kotlin/com/dprint/formatter/DprintFormattingTask.kt

Lines changed: 186 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.dprint.formatter
22

33
import com.dprint.i18n.DprintBundle
4+
import com.dprint.otel.AttributeKeys
5+
import com.dprint.otel.DprintScope
46
import com.dprint.services.editorservice.EditorServiceManager
57
import com.dprint.services.editorservice.FormatResult
68
import com.dprint.services.editorservice.exceptions.ProcessUnavailableException
@@ -11,6 +13,14 @@ import com.intellij.formatting.service.AsyncFormattingRequest
1113
import com.intellij.openapi.diagnostic.logger
1214
import com.intellij.openapi.project.Project
1315
import com.intellij.openapi.util.TextRange
16+
import com.intellij.platform.diagnostic.telemetry.TelemetryManager
17+
import com.intellij.platform.diagnostic.telemetry.helpers.use
18+
import io.opentelemetry.api.common.AttributeKey
19+
import io.opentelemetry.api.common.Attributes
20+
import io.opentelemetry.api.trace.Span
21+
import io.opentelemetry.api.trace.StatusCode
22+
import io.opentelemetry.api.trace.Tracer
23+
import io.opentelemetry.context.Context
1424
import java.util.concurrent.CancellationException
1525
import java.util.concurrent.CompletableFuture
1626
import java.util.concurrent.ExecutionException
@@ -28,72 +38,143 @@ class DprintFormattingTask(
2838
) {
2939
private var formattingIds = mutableListOf<Int>()
3040
private var isCancelled = false
41+
private val tracer: Tracer = TelemetryManager.getInstance().getTracer(DprintScope.FormatterScope)
3142

3243
/**
3344
* Used when we want to cancel a format, so that we can cancel every future in the chain.
3445
*/
3546
private val allFormatFutures = mutableListOf<CompletableFuture<FormatResult>>()
3647

3748
fun run() {
38-
val content = formattingRequest.documentText
39-
val ranges =
40-
if (editorServiceManager.canRangeFormat()) {
41-
formattingRequest.formattingRanges
42-
} else {
43-
mutableListOf(
44-
TextRange(0, content.length),
49+
val rootSpan =
50+
tracer.spanBuilder("dprint.format")
51+
.setAttribute(AttributeKeys.FILE_PATH, path)
52+
.startSpan()
53+
54+
try {
55+
rootSpan.makeCurrent().use { scope ->
56+
val content = formattingRequest.documentText
57+
58+
rootSpan.setAttribute(AttributeKeys.CONTENT_LENGTH, content.length.toLong())
59+
60+
val ranges =
61+
tracer.spanBuilder("dprint.determine_ranges")
62+
.startSpan().use { rangesSpan ->
63+
if (editorServiceManager.canRangeFormat()) {
64+
rangesSpan.setAttribute("range_format_supported", true)
65+
rangesSpan.setAttribute(
66+
"ranges_count",
67+
formattingRequest.formattingRanges.size.toLong(),
68+
)
69+
formattingRequest.formattingRanges
70+
} else {
71+
rangesSpan.setAttribute("range_format_supported", false)
72+
rangesSpan.setAttribute("ranges_count", 1L)
73+
mutableListOf(
74+
TextRange(0, content.length),
75+
)
76+
}
77+
}
78+
79+
infoLogWithConsole(
80+
DprintBundle.message("external.formatter.running.task", path),
81+
project,
82+
LOGGER,
4583
)
46-
}
4784

48-
infoLogWithConsole(
49-
DprintBundle.message("external.formatter.running.task", path),
50-
project,
51-
LOGGER,
52-
)
85+
val initialResult = FormatResult(formattedContent = content)
86+
val baseFormatFuture = CompletableFuture.completedFuture(initialResult)
87+
allFormatFutures.add(baseFormatFuture)
5388

54-
val initialResult = FormatResult(formattedContent = content)
55-
val baseFormatFuture = CompletableFuture.completedFuture(initialResult)
56-
allFormatFutures.add(baseFormatFuture)
57-
58-
var nextFuture = baseFormatFuture
59-
for (range in ranges.subList(0, ranges.size)) {
60-
nextFuture.thenCompose { formatResult ->
61-
nextFuture =
62-
if (isCancelled) {
63-
// Revert to the initial contents
64-
CompletableFuture.completedFuture(initialResult)
65-
} else {
66-
applyNextRangeFormat(
67-
path,
68-
formatResult,
69-
getStartOfRange(formatResult.formattedContent, content, range),
70-
getEndOfRange(formatResult.formattedContent, content, range),
71-
)
89+
val formatRangesSpan =
90+
tracer.spanBuilder("dprint.format_ranges")
91+
.setAttribute("ranges_count", ranges.size.toLong())
92+
.startSpan()
93+
94+
var nextFuture = baseFormatFuture
95+
for (range in ranges.subList(0, ranges.size)) {
96+
formatRangesSpan.addEvent(
97+
"processing_range",
98+
Attributes.of(
99+
AttributeKeys.RANGE_START,
100+
range.startOffset.toLong(),
101+
AttributeKeys.RANGE_END,
102+
range.endOffset.toLong(),
103+
),
104+
)
105+
106+
nextFuture.thenCompose { formatResult ->
107+
nextFuture =
108+
if (isCancelled) {
109+
formatRangesSpan.addEvent(
110+
"format_cancelled",
111+
Attributes.of(
112+
AttributeKeys.RANGE_START,
113+
range.startOffset.toLong(),
114+
AttributeKeys.RANGE_END,
115+
range.endOffset.toLong(),
116+
),
117+
)
118+
// Revert to the initial contents
119+
CompletableFuture.completedFuture(initialResult)
120+
} else {
121+
applyNextRangeFormat(
122+
path,
123+
formatResult,
124+
getStartOfRange(formatResult.formattedContent, content, range),
125+
getEndOfRange(formatResult.formattedContent, content, range),
126+
formatRangesSpan,
127+
)
128+
}
129+
nextFuture
72130
}
73-
nextFuture
74-
}
75-
}
131+
}
76132

77-
// Timeouts are handled at the EditorServiceManager level and an empty result will be
78-
// returned if something goes wrong
79-
val result = getFuture(nextFuture)
133+
// Timeouts are handled at the EditorServiceManager level and an empty result will be
134+
// returned if something goes wrong
135+
val result = getFuture(nextFuture)
136+
formatRangesSpan.end()
80137

81-
// If cancelled there is no need to utilise the formattingRequest finalising methods
82-
if (isCancelled) return
138+
// If cancelled there is no need to utilise the formattingRequest finalising methods
139+
if (isCancelled) {
140+
rootSpan.addEvent("formatting_cancelled")
141+
return
142+
}
83143

84-
// If the result is null we don't want to change the document text, so we just set it to be the original.
85-
// This should only happen if getting the future throws.
86-
if (result == null) {
87-
formattingRequest.onTextReady(content)
88-
return
89-
}
144+
val resultSpan =
145+
tracer.spanBuilder("dprint.process_result")
146+
.startSpan()
147+
148+
resultSpan.use {
149+
// If the result is null we don't want to change the document text, so we just set it to be the original.
150+
// This should only happen if getting the future throws.
151+
if (result == null) {
152+
resultSpan.setAttribute("result", "null")
153+
formattingRequest.onTextReady(content)
154+
return
155+
}
156+
157+
val error = result.error
158+
if (error != null) {
159+
resultSpan.setStatus(StatusCode.ERROR, error)
160+
formattingRequest.onError(DprintBundle.message("formatting.error"), error)
161+
} else {
162+
// Record if there was any content change
163+
resultSpan.setAttribute("content_changed", (result.formattedContent != content))
90164

91-
val error = result.error
92-
if (error != null) {
93-
formattingRequest.onError(DprintBundle.message("formatting.error"), error)
94-
} else {
95-
// If the result is a no op it will be null, in which case we pass the original content back in
96-
formattingRequest.onTextReady(result.formattedContent ?: content)
165+
// If the result is a no op it will be null, in which case we pass the original content back in
166+
val finalContent = result.formattedContent ?: content
167+
resultSpan.setAttribute("final_content_length", finalContent.length.toLong())
168+
formattingRequest.onTextReady(finalContent)
169+
}
170+
}
171+
}
172+
} catch (e: Exception) {
173+
rootSpan.recordException(e)
174+
rootSpan.setStatus(StatusCode.ERROR)
175+
throw e
176+
} finally {
177+
rootSpan.end()
97178
}
98179
}
99180

@@ -134,6 +215,7 @@ class DprintFormattingTask(
134215
previousFormatResult: FormatResult,
135216
startIndex: Int?,
136217
endIndex: Int?,
218+
parentSpan: Span,
137219
): CompletableFuture<FormatResult>? {
138220
val contentToFormat = previousFormatResult.formattedContent
139221
if (contentToFormat == null || startIndex == null || endIndex == null) {
@@ -150,15 +232,37 @@ class DprintFormattingTask(
150232
return null
151233
}
152234

235+
val span =
236+
tracer.spanBuilder("dprint.formatter.apply_range_format")
237+
.setParent(Context.current().with(parentSpan))
238+
.setAttribute(AttributeKeys.FILE_PATH, path)
239+
.setAttribute(AttributeKeys.RANGE_START, startIndex.toLong())
240+
.setAttribute(AttributeKeys.RANGE_END, endIndex.toLong())
241+
.setAttribute(AttributeKeys.CONTENT_LENGTH, contentToFormat.length.toLong())
242+
.startSpan()
243+
153244
// Need to update the formatting id so the correct job would be cancelled
154245
val formattingId = editorServiceManager.maybeGetFormatId()
155246
formattingId?.let {
156247
formattingIds.add(it)
248+
span.setAttribute(AttributeKeys.FORMATTING_ID, it.toLong())
157249
}
158250

159251
val nextFuture = CompletableFuture<FormatResult>()
160252
allFormatFutures.add(nextFuture)
161253
val nextHandler: (FormatResult) -> Unit = { nextResult ->
254+
// Add result information to the span
255+
if (nextResult.error != null) {
256+
span.setStatus(StatusCode.ERROR, nextResult.error)
257+
} else {
258+
span.setStatus(StatusCode.OK)
259+
if (nextResult.formattedContent != null) {
260+
val contentLengthDiff = nextResult.formattedContent.length - contentToFormat.length
261+
span.setAttribute("content_length_diff", contentLengthDiff.toLong())
262+
}
263+
}
264+
span.end()
265+
162266
nextFuture.complete(nextResult)
163267
}
164268
editorServiceManager.format(
@@ -174,21 +278,40 @@ class DprintFormattingTask(
174278
}
175279

176280
fun cancel(): Boolean {
177-
if (!editorServiceManager.canCancelFormat()) return false
281+
val span =
282+
tracer.spanBuilder("dprint.formatter.cancel_format")
283+
.setAttribute(AttributeKeys.FILE_PATH, path)
284+
.startSpan()
178285

179-
isCancelled = true
180-
for (id in formattingIds) {
181-
infoLogWithConsole(
182-
DprintBundle.message("external.formatter.cancelling.task", id),
183-
project,
184-
LOGGER,
286+
span.use { scope ->
287+
if (!editorServiceManager.canCancelFormat()) {
288+
span.setAttribute("can_cancel", false)
289+
span.addEvent("cancel_not_supported")
290+
return false
291+
}
292+
293+
span.setAttribute("can_cancel", true)
294+
span.setAttribute("formatting_ids_count", formattingIds.size.toLong())
295+
296+
isCancelled = true
297+
for (id in formattingIds) {
298+
span.addEvent("cancelling_task", Attributes.of(AttributeKeys.FORMATTING_ID, id.toLong()))
299+
infoLogWithConsole(
300+
DprintBundle.message("external.formatter.cancelling.task", id),
301+
project,
302+
LOGGER,
303+
)
304+
editorServiceManager.cancelFormat(id)
305+
}
306+
307+
// Clean up state so process can complete
308+
span.addEvent(
309+
"cancelling_futures",
310+
Attributes.of(AttributeKey.longKey("futures_count"), allFormatFutures.size.toLong()),
185311
)
186-
editorServiceManager.cancelFormat(id)
312+
allFormatFutures.stream().forEach { f -> f.cancel(true) }
313+
return true
187314
}
188-
189-
// Clean up state so process can complete
190-
allFormatFutures.stream().forEach { f -> f.cancel(true) }
191-
return true
192315
}
193316

194317
fun isRunUnderProgress(): Boolean {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.dprint.otel
2+
3+
import io.opentelemetry.api.common.AttributeKey
4+
5+
/** OpenTelemetry attribute keys */
6+
object AttributeKeys {
7+
val FILE_PATH = AttributeKey.stringKey("file.path")
8+
val CONFIG_PATH = AttributeKey.stringKey("config.path")
9+
val SCHEMA_VERSION = AttributeKey.longKey("schema.version")
10+
val TIMEOUT_MS = AttributeKey.longKey("timeout.ms")
11+
val RANGE_START = AttributeKey.longKey("range.start")
12+
val RANGE_END = AttributeKey.longKey("range.end")
13+
val FORMATTING_ID = AttributeKey.longKey("formatting.id")
14+
val CONTENT_LENGTH = AttributeKey.longKey("content.length")
15+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.dprint.otel
2+
3+
import com.intellij.platform.diagnostic.telemetry.Scope
4+
5+
object DprintScope {
6+
val FormatterScope = Scope("com.dprint.formatter")
7+
val EditorServiceScope = Scope("com.dprint.editorservice")
8+
}

0 commit comments

Comments
 (0)