File: Image\OpenTelemetryImageGeneratorTests.cs
Web Access
Project: src\test\Libraries\Microsoft.Extensions.AI.Tests\Microsoft.Extensions.AI.Tests.csproj (Microsoft.Extensions.AI.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using OpenTelemetry.Trace;
using Xunit;
 
namespace Microsoft.Extensions.AI;
 
public class OpenTelemetryImageGeneratorTests
{
    [Fact]
    public void InvalidArgs_Throws()
    {
        Assert.Throws<ArgumentNullException>("innerGenerator", () => new OpenTelemetryImageGenerator(null!));
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task ExpectedInformationLogged_Async(bool enableSensitiveData)
    {
        var sourceName = Guid.NewGuid().ToString();
        var activities = new List<Activity>();
        using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
            .AddSource(sourceName)
            .AddInMemoryExporter(activities)
            .Build();
 
        using var innerGenerator = new TestImageGenerator
        {
            GenerateImagesAsyncCallback = async (request, options, cancellationToken) =>
            {
                await Task.Yield();
 
                return new()
                {
                    Contents =
                    [
                        new UriContent("http://example/output.png", "image/png"),
                        new DataContent(new byte[] { 1, 2, 3, 4 }, "image/png") { Name = "moreOutput.png" },
                    ],
 
                    Usage = new()
                    {
                        InputTokenCount = 10,
                        OutputTokenCount = 20,
                        TotalTokenCount = 30,
                    },
                };
            },
 
            GetServiceCallback = (serviceType, serviceKey) =>
                serviceType == typeof(ImageGeneratorMetadata) ? new ImageGeneratorMetadata("testservice", new Uri("http://localhost:12345/something"), "amazingmodel") :
                null,
        };
 
        using var g = innerGenerator
            .AsBuilder()
            .UseOpenTelemetry(null, sourceName, configure: instance =>
            {
                instance.EnableSensitiveData = enableSensitiveData;
            })
            .Build();
 
        ImageGenerationRequest request = new()
        {
            Prompt = "This is the input prompt.",
            OriginalImages = [new UriContent("http://example/input.png", "image/png")],
        };
 
        ImageGenerationOptions options = new()
        {
            Count = 2,
            ImageSize = new(1024, 768),
            MediaType = "image/jpeg",
            ModelId = "mycoolimagemodel",
            AdditionalProperties = new()
            {
                ["service_tier"] = "value1",
                ["SomethingElse"] = "value2",
            },
        };
 
        await g.GenerateAsync(request, options);
 
        var activity = Assert.Single(activities);
 
        Assert.NotNull(activity.Id);
        Assert.NotEmpty(activity.Id);
 
        Assert.Equal("localhost", activity.GetTagItem("server.address"));
        Assert.Equal(12345, (int)activity.GetTagItem("server.port")!);
 
        Assert.Equal("generate_content mycoolimagemodel", activity.DisplayName);
        Assert.Equal("testservice", activity.GetTagItem("gen_ai.provider.name"));
 
        Assert.Equal("mycoolimagemodel", activity.GetTagItem("gen_ai.request.model"));
        Assert.Equal(2, activity.GetTagItem("gen_ai.request.choice.count"));
        Assert.Equal(1024, activity.GetTagItem("gen_ai.request.image.width"));
        Assert.Equal(768, activity.GetTagItem("gen_ai.request.image.height"));
        Assert.Equal(enableSensitiveData ? "value1" : null, activity.GetTagItem("service_tier"));
        Assert.Equal(enableSensitiveData ? "value2" : null, activity.GetTagItem("SomethingElse"));
 
        Assert.Equal(10, activity.GetTagItem("gen_ai.usage.input_tokens"));
        Assert.Equal(20, activity.GetTagItem("gen_ai.usage.output_tokens"));
 
        Assert.True(activity.Duration.TotalMilliseconds > 0);
 
        var tags = activity.Tags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
        if (enableSensitiveData)
        {
            Assert.Equal(ReplaceWhitespace("""
                [
                  {
                    "role": "user",
                    "parts": [
                      {
                        "type": "text",
                        "content": "This is the input prompt."
                      },
                      {
                        "type": "image",
                        "content": "http://example/input.png"
                      }
                    ]
                  }
                ]
                """), ReplaceWhitespace(tags["gen_ai.input.messages"]));
 
            Assert.Equal(ReplaceWhitespace("""
                [
                  {
                    "role": "assistant",
                    "parts": [
                      {
                        "type": "image",
                        "content": "http://example/output.png"
                      },
                      {
                        "type": "image",
                        "content": "data:image/png;base64,AQIDBA=="
                      }
                    ]
                  }
                ]
                """), ReplaceWhitespace(tags["gen_ai.output.messages"]));
        }
        else
        {
            Assert.False(tags.ContainsKey("gen_ai.input.messages"));
            Assert.False(tags.ContainsKey("gen_ai.output.messages"));
        }
 
        static string ReplaceWhitespace(string? input) => Regex.Replace(input ?? "", @"\s+", " ").Trim();
    }
}