forked from npgsql/npgsql
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathNpgsqlActivitySource.cs
More file actions
188 lines (156 loc) · 7.33 KB
/
NpgsqlActivitySource.cs
File metadata and controls
188 lines (156 loc) · 7.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
using Npgsql.Internal;
using System;
using System.Data;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
namespace Npgsql;
// Semantic conventions for database client spans: https://opentelemetry.io/docs/specs/semconv/database/database-spans/
// Semantic conventions for PostgreSQL client operations: https://opentelemetry.io/docs/specs/semconv/database/postgresql/
static class NpgsqlActivitySource
{
static readonly ActivitySource Source = new("Npgsql", GetLibraryVersion());
internal static bool IsEnabled => Source.HasListeners();
internal static Activity? CommandStart(string commandText, CommandType commandType, bool? prepared, string? spanName)
{
string? operationName = null;
switch (commandType)
{
case CommandType.StoredProcedure:
// We follow the {db.operation.name} {target} pattern of the spec, with the operation being SELECT/CALL and
// the target being the stored procedure name.
operationName = NpgsqlCommand.EnableStoredProcedureCompatMode ? "SELECT" : "CALL";
spanName ??= $"{operationName} {commandText}";
break;
case CommandType.TableDirect:
// We follow the {db.operation.name} {target} pattern of the spec, with the operation being SELECT and
// the target being the table (collection) name.
operationName = "SELECT";
spanName ??= $"{operationName} {commandText}";
break;
case CommandType.Text:
// We don't have db.query.summary, db.operation.name or target (without parsing SQL),
// so we fall back to db.system.name as per the specs.
spanName ??= "postgresql";
break;
default:
throw new ArgumentOutOfRangeException(nameof(commandType), commandType, null);
}
var activity = Source.StartActivity(spanName, ActivityKind.Client);
if (activity is not { IsAllDataRequested: true })
return activity;
activity.SetTag("db.query.text", commandText);
if (prepared is true)
activity.SetTag("db.npgsql.prepared", true);
switch (commandType)
{
case CommandType.StoredProcedure:
Debug.Assert(operationName is not null);
activity.SetTag("db.operation.name", operationName);
activity.SetTag("db.stored_procedure.name", commandText);
break;
case CommandType.TableDirect:
Debug.Assert(operationName is not null);
activity.SetTag("db.operation.name", operationName);
activity.SetTag("db.collection.name", commandText);
break;
}
return activity;
}
internal static Activity? PhysicalConnectionOpen(NpgsqlConnector connector)
{
if (!connector.DataSource.Configuration.TracingOptions.EnablePhysicalOpenTracing)
return null;
// Note that physical connection open is not part of the OpenTelemetry spec.
// We emit it if enabled, following the general name/tags guidelines.
var dbName = connector.Settings.Database ?? connector.InferredUserName;
var activity = Source.StartActivity("CONNECT " + dbName, ActivityKind.Client);
if (activity is not { IsAllDataRequested: true })
return activity;
// We set these basic tags on the activity so that they're populated even when the physical open fails.
activity.SetTag("db.system.name", "postgresql");
activity.SetTag("db.npgsql.data_source", connector.DataSource.Name);
return activity;
}
internal static void Enrich(Activity activity, NpgsqlConnector connector)
{
if (!activity.IsAllDataRequested)
return;
activity.SetTag("db.system.name", "postgresql");
// TODO: For now, we only set the database name, without adding the first schema in the search_path
// as per the PG tracing specs (https://opentelemetry.io/docs/specs/semconv/database/postgresql/).
// See #6336
activity.SetTag("db.namespace", connector.Settings.Database ?? connector.InferredUserName);
var endPoint = connector.ConnectedEndPoint;
Debug.Assert(endPoint is not null);
activity.SetTag("server.address", connector.Host);
switch (endPoint)
{
case IPEndPoint ipEndPoint:
if (ipEndPoint.Port != 5432)
activity.SetTag("server.port", ipEndPoint.Port);
break;
case UnixDomainSocketEndPoint:
break;
default:
throw new UnreachableException("Invalid endpoint type: " + endPoint.GetType());
}
// Npgsql-specific tags
activity.SetTag("db.npgsql.data_source", connector.DataSource.Name);
activity.SetTag("db.npgsql.connection_id", connector.Id);
}
internal static void ReceivedFirstResponse(Activity activity, NpgsqlTracingOptions tracingOptions)
{
if (!activity.IsAllDataRequested || !tracingOptions.EnableFirstResponseEvent)
return;
var activityEvent = new ActivityEvent("received-first-response");
activity.AddEvent(activityEvent);
}
internal static void SetException(Activity activity, Exception exception, bool escaped = true)
{
activity.AddException(exception);
if (exception is PostgresException { SqlState: var sqlState })
{
activity.SetTag("db.response.status_code", sqlState);
// error.type SHOULD match the db.response.status_code returned by the database or the client library, or the canonical name of exception that occurred.
// Since we don't have a table to map the error code to a textual description, the SQL state is the best we can do.
activity.SetTag("error.type", sqlState);
}
else
{
if (exception is NpgsqlException { InnerException: Exception innerException })
exception = innerException;
activity.SetTag("error.type", exception.GetType().FullName);
}
var statusDescription = exception is PostgresException pgEx ? pgEx.SqlState : exception.Message;
activity.SetStatus(ActivityStatusCode.Error, statusDescription);
activity.Dispose();
}
internal static Activity? CopyStart(string command, NpgsqlConnector connector, string? spanName, string operation)
{
var activity = Source.StartActivity(spanName ?? operation, ActivityKind.Client);
if (activity is not { IsAllDataRequested: true })
return activity;
activity.SetTag("db.query.text", command);
activity.SetTag("db.operation.name", operation);
Enrich(activity, connector);
return activity;
}
internal static void SetOperation(Activity activity, string operation)
{
if (!activity.IsAllDataRequested)
return;
activity.SetTag("db.operation.name", operation);
}
internal static void CopyStop(Activity activity, ulong? rows = null)
{
if (rows.HasValue)
activity.SetTag("db.npgsql.rows", rows.Value);
activity.Dispose();
}
static string GetLibraryVersion()
=> typeof(NpgsqlDataSource).Assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
.InformationalVersion ?? "UNKNOWN";
}