Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
* Updated OpenTelemetry core component version(s) to `1.15.2`.
([#4080](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4080))

* Fix `IndexOutOfRangeException` when parsing SQL statements.
([#4139](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4139))

## 1.15.0-beta.1

Released 2026-Jan-21
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
<PackageReference Include="OpenTelemetry.Api.ProviderBuilderExtensions" />
</ItemGroup>

<PropertyGroup>
<DefineConstants>$(DefineConstants);INCLUDE_SQL_QUERY_PARSER</DefineConstants>
</PropertyGroup>

<ItemGroup>
<Compile Include="$(RepoRoot)\src\Shared\AssemblyVersionExtensions.cs" Link="Includes\AssemblyVersionExtensions.cs" />
<Compile Include="$(RepoRoot)\src\Shared\DatabaseSemanticConventionHelper.cs" Link="Includes\DatabaseSemanticConventionHelper.cs" />
Expand Down
3 changes: 3 additions & 0 deletions src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
* Updated OpenTelemetry core component version(s) to `1.15.2`.
([#4080](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4080))

* Fix `IndexOutOfRangeException` when parsing SQL statements.
([#4139](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4139))

## 1.15.1

Released 2026-Mar-04
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
<IsAotCompatible>false</IsAotCompatible>
</PropertyGroup>

<PropertyGroup>
<DefineConstants>$(DefineConstants);INCLUDE_SQL_QUERY_PARSER</DefineConstants>
</PropertyGroup>

<ItemGroup>
<Compile Include="$(RepoRoot)\src\Shared\ActivitySourceFactory.cs" Link="Includes\ActivitySourceFactory.cs" />
<Compile Include="$(RepoRoot)\src\Shared\AssemblyVersionExtensions.cs" Link="Includes\AssemblyVersionExtensions.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@
<Compile Include="$(RepoRoot)\src\Shared\NullableAttributes.cs" Link="Includes\NullableAttributes.cs" />
<Compile Include="$(RepoRoot)\src\Shared\PropertyFetcher.cs" Link="Includes\PropertyFetcher.cs" />
<Compile Include="$(RepoRoot)\src\Shared\SemanticConventions.cs" Link="Includes\SemanticConventions.cs" />
<Compile Include="$(RepoRoot)\src\Shared\SqlProcessor.cs" Link="Includes\SqlProcessor.cs" />
<Compile Include="$(RepoRoot)\src\Shared\SqlStatementInfo.cs" Link="Includes\SqlStatementInfo.cs" />

</ItemGroup>

Expand Down
6 changes: 6 additions & 0 deletions src/Shared/DatabaseSemanticConventionHelper.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

#if INCLUDE_SQL_QUERY_PARSER
using System.Diagnostics;
#endif
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Configuration;
#if INCLUDE_SQL_QUERY_PARSER
using OpenTelemetry.Instrumentation;
using OpenTelemetry.Trace;
#endif

namespace OpenTelemetry.Internal;

Expand Down Expand Up @@ -59,6 +63,7 @@ public static DatabaseSemanticConvention GetSemanticConventionOptIn(IConfigurati
return DatabaseSemanticConvention.Old;
}

#if INCLUDE_SQL_QUERY_PARSER
public static void ApplyConventionsForQueryText(
Activity activity,
string? commandText,
Expand Down Expand Up @@ -151,6 +156,7 @@ public static void AddTagsForSamplingAndUpdateActivityNameForStoredProcedure(
tagsList.Add(SemanticConventions.AttributeDbQuerySummary, dbQuerySummary);
activityName = dbQuerySummary;
}
#endif

private static bool TryGetConfiguredValues(IConfiguration configuration, [NotNullWhen(true)] out HashSet<string>? values)
{
Expand Down
66 changes: 56 additions & 10 deletions src/Shared/SqlProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -306,12 +306,59 @@ private static SqlStatementInfo SanitizeSql(string sql)
sanitizedSql,
summary.Slice(0, summaryLength).ToString());

if (state.RentedSummaryBuffer != null)
{
ArrayPool<char>.Shared.Return(state.RentedSummaryBuffer);
}

// We don't clear the buffer as we know the content has been sanitized
ArrayPool<char>.Shared.Return(rentedBuffer);

return sqlStatementInfo;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AppendSummaryChar(char value, ref ParseState state)
{
EnsureSummaryCapacity(checked(state.SummaryPosition + 1), ref state);

state.SummaryBuffer[state.SummaryPosition++] = value;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AppendSummaryToken(ReadOnlySpan<char> value, ref ParseState state)
{
EnsureSummaryCapacity(checked(state.SummaryPosition + value.Length), ref state);

value.CopyTo(state.SummaryBuffer.Slice(state.SummaryPosition));

state.SummaryPosition += value.Length;
}

private static void EnsureSummaryCapacity(int requiredCapacity, ref ParseState state)
{
if (requiredCapacity <= state.SummaryBuffer.Length)
{
return;
}

var doubledCapacity = state.SummaryBuffer.Length <= (int.MaxValue / 2)
? state.SummaryBuffer.Length * 2
: int.MaxValue;

var newBuffer = ArrayPool<char>.Shared.Rent(Math.Max(requiredCapacity, doubledCapacity));

state.SummaryBuffer.Slice(0, state.SummaryPosition).CopyTo(newBuffer);

if (state.RentedSummaryBuffer != null)
{
ArrayPool<char>.Shared.Return(state.RentedSummaryBuffer);
}

state.RentedSummaryBuffer = newBuffer;
state.SummaryBuffer = newBuffer.AsSpan();
}

private static void ParseNextTokenFast(
ReadOnlySpan<char> sql,
Span<char> buffer,
Expand Down Expand Up @@ -436,11 +483,10 @@ private static void ParseNextToken(
state.FirstSummaryKeyword = potentialKeywordInfo.SqlKeyword;
}

sql.Slice(start, keywordLength).CopyTo(state.SummaryBuffer.Slice(state.SummaryPosition));
state.SummaryPosition += keywordLength;
AppendSummaryToken(sql.Slice(start, keywordLength), ref state);

// Add a space after the keyword. The trailing space will be trimmed later.
state.SummaryBuffer[state.SummaryPosition++] = ' ';
AppendSummaryChar(SpaceChar, ref state);

state.PreviousSummaryKeyword = potentialKeywordInfo.SqlKeyword;
}
Expand Down Expand Up @@ -521,11 +567,10 @@ private static void ParseNextToken(
// Optionally copy to summary buffer.
if (state.CaptureNextNonKeywordTokenAsIdentifier)
{
sql.Slice(start, length).CopyTo(state.SummaryBuffer.Slice(state.SummaryPosition));
state.SummaryPosition += length;
AppendSummaryToken(sql.Slice(start, length), ref state);

// Add a space after the identifier. The trailing space will be trimmed later.
state.SummaryBuffer[state.SummaryPosition++] = SpaceChar;
AppendSummaryChar(SpaceChar, ref state);
}
}

Expand All @@ -548,16 +593,16 @@ private static void ParseNextToken(
// Remove the space we added after the identifier in the summary buffer before we write the closing bracket.
state.SummaryPosition--;

state.SummaryBuffer[state.SummaryPosition++] = CloseSquareBracketChar;
AppendSummaryChar(CloseSquareBracketChar, ref state);

var nextPos = state.ParsePosition + 1;
if (nextPos >= sql.Length || sql[nextPos] != DotChar)
{
state.SummaryBuffer[state.SummaryPosition++] = SpaceChar;
AppendSummaryChar(SpaceChar, ref state);
}
else
{
state.SummaryBuffer[state.SummaryPosition++] = DotChar; // write the dot to summary
AppendSummaryChar(DotChar, ref state); // write the dot to summary
}
}

Expand All @@ -569,7 +614,7 @@ private static void ParseNextToken(
if (state.CaptureNextNonKeywordTokenAsIdentifier && currentChar is OpenSquareBracketChar)
{
state.InEscapedIdentifier = true;
state.SummaryBuffer[state.SummaryPosition++] = OpenSquareBracketChar;
AppendSummaryChar(OpenSquareBracketChar, ref state);
}

buffer[state.SanitizedPosition++] = currentChar;
Expand Down Expand Up @@ -908,6 +953,7 @@ private ref struct ParseState

// Stored in state to avoid slicing repeatedly.
public Span<char> SummaryBuffer;
public char[]? RentedSummaryBuffer;

/// <summary>
/// Will be set if a keyword has been matched by the parser.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,21 @@ public static void GetSanitizedSql_Summary_Length_Limited(NonEmptyString input)
Assert.True(actual.DbQuerySummary.Length <= 255);
}

[Property(MaxTest = MaxValue)]
public static void GetSanitizedSql_SqlStatement_AlignedToArrayPoolBuckets_DoesNotThrow(PositiveInt seed)
{
// Choose statement lengths that align with common ArrayPool buckets. Before the fix,
// these lengths can make the initial summary slice exactly as long as the SQL text.
var sqlLength = 1 << ((seed.Get % 5) + 4);
var prefix = "CREATE TABLE ";
var identifier = new string('X', sqlLength - prefix.Length);
var sql = prefix + identifier;

var exception = Record.Exception(() => SqlProcessor.GetSanitizedSql(sql));

Assert.Null(exception);
}

[Property(MaxTest = MaxValue)]
public static void GetSanitizedSql_In_Clause_Optimizes_Sanitization(PositiveInt input)
{
Expand Down
11 changes: 11 additions & 0 deletions test/OpenTelemetry.Contrib.Shared.Tests/SqlProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ public SqlProcessorTests(ITestOutputHelper output)

public static TheoryData<SqlProcessorTestCases.TestCase> TestData => SqlProcessorTestCases.GetSemanticConventionsTestCases();

[Fact]
public void GetSanitizedSql_CreateTableWithTrailingIdentifier_DoesNotThrow()
{
var sql = "CREATE TABLE XXX";

var sqlStatementInfo = SqlProcessor.GetSanitizedSql(sql);

Assert.Equal(sql, sqlStatementInfo.SanitizedSql);
Assert.Equal(sql, sqlStatementInfo.DbQuerySummary);
}
Comment thread
martincostello marked this conversation as resolved.

[SkippableTheory]
[MemberData(nameof(TestData))]
public void TestGetSanitizedSql(SqlProcessorTestCases.TestCase testCase)
Expand Down
Loading