//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.SqlServer.Management.Sdk.Sfc;
using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlServer.Management.XEvent;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.Profiler
{
///
/// Main class for Profiler Service functionality
///
public sealed class ProfilerService : IDisposable, IXEventSessionFactory, IProfilerSessionListener
{
private bool disposed;
private ConnectionService connectionService = null;
private ProfilerSessionMonitor monitor = new ProfilerSessionMonitor();
private static readonly Lazy instance = new Lazy(() => new ProfilerService());
///
/// Construct a new ProfilerService instance with default parameters
///
public ProfilerService()
{
this.XEventSessionFactory = this;
}
///
/// Gets the singleton instance object
///
public static ProfilerService Instance
{
get { return instance.Value; }
}
///
/// Internal for testing purposes only
///
internal ConnectionService ConnectionServiceInstance
{
get
{
if (connectionService == null)
{
connectionService = ConnectionService.Instance;
}
return connectionService;
}
set
{
connectionService = value;
}
}
///
/// XEvent session factory. Internal to allow mocking in unit tests.
///
internal IXEventSessionFactory XEventSessionFactory { get; set; }
///
/// Session monitor instance
///
internal ProfilerSessionMonitor SessionMonitor
{
get
{
return this.monitor;
}
}
///
/// Service host object for sending/receiving requests/events.
/// Internal for testing purposes.
///
internal IProtocolEndpoint ServiceHost
{
get;
set;
}
///
/// Initializes the Profiler Service instance
///
public void InitializeService(ServiceHost serviceHost)
{
this.ServiceHost = serviceHost;
this.ServiceHost.SetRequestHandler(StartProfilingRequest.Type, HandleStartProfilingRequest);
this.ServiceHost.SetRequestHandler(StopProfilingRequest.Type, HandleStopProfilingRequest);
this.ServiceHost.SetRequestHandler(PauseProfilingRequest.Type, HandlePauseProfilingRequest);
this.SessionMonitor.AddSessionListener(this);
}
///
/// Handle request to start a profiling session
///
internal async Task HandleStartProfilingRequest(StartProfilingParams parameters, RequestContext requestContext)
{
try
{
var result = new StartProfilingResult();
ConnectionInfo connInfo;
ConnectionServiceInstance.TryFindConnection(
parameters.OwnerUri,
out connInfo);
if (connInfo != null)
{
int xEventSessionId = StartSession(parameters.OwnerUri, parameters.TemplateName, connInfo);
result.SessionId = xEventSessionId.ToString();
result.Succeeded = true;
}
else
{
result.Succeeded = false;
result.ErrorMessage = SR.ProfilerConnectionNotFound;
}
await requestContext.SendResult(result);
}
catch (Exception e)
{
await requestContext.SendError(e);
}
}
///
/// Handle request to stop a profiling session
///
internal async Task HandleStopProfilingRequest(StopProfilingParams parameters, RequestContext requestContext)
{
try
{
ProfilerSession session;
monitor.StopMonitoringSession(parameters.OwnerUri, out session);
session.XEventSession.Stop();
await requestContext.SendResult(new StopProfilingResult{});
}
catch (Exception e)
{
await requestContext.SendError(e);
}
}
///
/// Handle request to pause a profiling session
///
internal async Task HandlePauseProfilingRequest(PauseProfilingParams parameters, RequestContext requestContext)
{
try
{
monitor.PauseViewer(parameters.OwnerUri);
await requestContext.SendResult(new PauseProfilingResult{});
}
catch (Exception e)
{
await requestContext.SendError(e);
}
}
///
/// Starts a new profiler session or connects to an existing session
/// for the provided connection and template info
///
///
/// The XEvent Session Id that was started
///
internal int StartSession(string ownerUri, string template, ConnectionInfo connInfo)
{
// create a new XEvent session and Profiler session
var xeSession = this.XEventSessionFactory.GetOrCreateXEventSession(template, connInfo);
// start monitoring the profiler session
monitor.StartMonitoringSession(ownerUri, xeSession);
return xeSession.Id;
}
///
/// Gets or creates an XEvent session with the given template per the IXEventSessionFactory contract
/// Also starts the session if it isn't currently running
///
public IXEventSession GetOrCreateXEventSession(string template, ConnectionInfo connInfo)
{
string sessionName = "Profiler";
var sqlConnection = ConnectionService.OpenSqlConnection(connInfo);
SqlStoreConnection connection = new SqlStoreConnection(sqlConnection);
XEStore store = new XEStore(connection);
Session session = store.Sessions[sessionName];
// start the session if it isn't already running
if (session == null)
{
session = CreateSession(connection, sessionName);
}
if (session != null && !session.IsRunning)
{
session.Start();
}
// create xevent session wrapper
return new XEventSession()
{
Session = session
};
}
private static Session CreateSession(SqlStoreConnection connection, string sessionName)
{
string createSessionSql =
@"
CREATE EVENT SESSION [Profiler] ON SERVER
ADD EVENT sqlserver.attention(
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.database_id,sqlserver.nt_username,sqlserver.query_hash,sqlserver.server_principal_name,sqlserver.session_id)
WHERE ([package0].[equal_boolean]([sqlserver].[is_system],(0)))),
ADD EVENT sqlserver.existing_connection(SET collect_options_text=(1)
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.nt_username,sqlserver.server_principal_name,sqlserver.session_id)),
ADD EVENT sqlserver.login(SET collect_options_text=(1)
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.nt_username,sqlserver.server_principal_name,sqlserver.session_id)),
ADD EVENT sqlserver.logout(
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.nt_username,sqlserver.server_principal_name,sqlserver.session_id)),
ADD EVENT sqlserver.rpc_completed(
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.database_id,sqlserver.nt_username,sqlserver.query_hash,sqlserver.server_principal_name,sqlserver.session_id)
WHERE ([package0].[equal_boolean]([sqlserver].[is_system],(0)))),
ADD EVENT sqlserver.sql_batch_completed(
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.database_id,sqlserver.nt_username,sqlserver.query_hash,sqlserver.server_principal_name,sqlserver.session_id)
WHERE ([package0].[equal_boolean]([sqlserver].[is_system],(0)))),
ADD EVENT sqlserver.sql_batch_starting(
ACTION(package0.event_sequence,sqlserver.client_app_name,sqlserver.client_pid,sqlserver.database_id,sqlserver.nt_username,sqlserver.query_hash,sqlserver.server_principal_name,sqlserver.session_id)
WHERE ([package0].[equal_boolean]([sqlserver].[is_system],(0))))
ADD TARGET package0.ring_buffer(SET max_events_limit=(1000),max_memory=(51200))
WITH (MAX_MEMORY=8192 KB,EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS,MAX_DISPATCH_LATENCY=5 SECONDS,MAX_EVENT_SIZE=0 KB,MEMORY_PARTITION_MODE=PER_CPU,TRACK_CAUSALITY=ON,STARTUP_STATE=OFF)";
connection.ServerConnection.ExecuteNonQuery(createSessionSql);
XEStore store = new XEStore(connection);
return store.Sessions[sessionName];
}
///
/// Callback when profiler events are available
///
public void EventsAvailable(string sessionId, List events, bool eventsLost)
{
// pass the profiler events on to the client
this.ServiceHost.SendEvent(
ProfilerEventsAvailableNotification.Type,
new ProfilerEventsAvailableParams()
{
OwnerUri = sessionId,
Events = events,
EventsLost = eventsLost
});
}
///
/// Callback when the XEvent session is closed unexpectedly
///
public void SessionStopped(string viewerId, int sessionId)
{
// notify the client that their session closed
this.ServiceHost.SendEvent(
ProfilerSessionStoppedNotification.Type,
new ProfilerSessionStoppedParams()
{
OwnerUri = viewerId,
SessionId = sessionId
});
}
///
/// Disposes the Profiler Service
///
public void Dispose()
{
if (!disposed)
{
disposed = true;
}
}
}
}