diff --git a/ldapserver/server.py b/ldapserver/server.py
index bbe8dbf157057618a523405d77de5813ada217c4..a629a5c5929c990397e1eb65b8d93063b34cd202 100644
--- a/ldapserver/server.py
+++ b/ldapserver/server.py
@@ -2,6 +2,10 @@ import traceback
 import ssl
 import socketserver
 import typing
+import logging
+import time
+import random
+import string
 
 from . import asn1, exceptions, ldap, schema, objects
 from .dn import DN
@@ -11,12 +15,22 @@ def reject_critical_controls(controls=None):
 		if control.criticality:
 			raise exceptions.LDAPUnavailableCriticalExtension()
 
+class RequestLogAdapter(logging.LoggerAdapter):
+	def process(self, msg, kwargs):
+		return self.extra['trace_id'] + ': ' + msg, kwargs
+
 class BaseLDAPRequestHandler(socketserver.BaseRequestHandler):
+	logger = logging.getLogger('ldapserver.server')
+
 	def setup(self):
 		super().setup()
+		self.trace_id = ''.join([random.choice(string.ascii_letters) for _ in range(10)])
+		self.logger = RequestLogAdapter(self.logger, {'trace_id': self.trace_id})
 		self.keep_running = True
 
 	def handle(self):
+		time_connect = time.perf_counter()
+		self.logger.info('Connection from %r', self.client_address)
 		buf = b''
 		while self.keep_running:
 			try:
@@ -31,6 +45,8 @@ class BaseLDAPRequestHandler(socketserver.BaseRequestHandler):
 				else:
 					buf += chunk
 		self.request.close()
+		time_disconnect = time.perf_counter()
+		self.logger.info('Disconnected duration_seconds=%.3f', time_disconnect - time_connect)
 
 	def handle_message(self, shallowmsg: ldap.ShallowLDAPMessage) -> typing.Iterable[ldap.LDAPMessage]:
 		'''Handle an LDAP request foobar
@@ -57,7 +73,7 @@ class BaseLDAPRequestHandler(socketserver.BaseRequestHandler):
 			try:
 				msg = shallowmsg.decode()[0]
 			except ValueError as e:
-				self.on_recv_invalid(shallowmsg)
+				self.logger.error('Could not decode message %s, ignoring', shallowmsg)
 				raise exceptions.LDAPProtocolError() from e
 			for args in handler(msg.protocolOp, msg.controls):
 				response, controls = args if isinstance(args, tuple) else (args, None)
@@ -66,54 +82,62 @@ class BaseLDAPRequestHandler(socketserver.BaseRequestHandler):
 			if response_type is not None:
 				respmsg = ldap.LDAPMessage(shallowmsg.messageID, response_type(e.code, diagnosticMessage=e.message))
 				yield respmsg
+				self.logger.info('Operation aborted, responded with result code "%s" msg="%s"', e.code.name, e.message)
 		except Exception as e: # pylint: disable=broad-except
 			if response_type is not None:
 				respmsg = ldap.LDAPMessage(shallowmsg.messageID, response_type(ldap.LDAPResultCode.other))
 				yield respmsg
-			self.on_exception(e)
-
-	def on_recv_invalid(self, shallowmsg):
-		pass
-
-	def on_exception(self, e):
-		traceback.print_exc()
+				self.logger.exception('Uncaught exception, responded with result code "other"')
+			else:
+				self.logger.exception('Uncaught exception, ignored request')
 
 	def handle_bind(self, op: ldap.BindRequest, controls=None) -> typing.Iterable[ldap.ProtocolOp]:
+		self.logger.info('BIND %s', op)
 		reject_critical_controls(controls)
 		raise exceptions.LDAPAuthMethodNotSupported()
 
 	def handle_unbind(self, op: ldap.UnbindRequest, controls=None) -> typing.NoReturn:
+		self.logger.info('UNBIND %s', op)
 		reject_critical_controls(controls)
 		self.keep_running = False
+		return []
 
 	def handle_search(self, op: ldap.SearchRequest, controls=None) -> typing.Iterable[ldap.ProtocolOp]:
+		self.logger.info('SEARCH %s', op)
 		reject_critical_controls(controls)
 		yield ldap.SearchResultDone(ldap.LDAPResultCode.success)
 
 	def handle_modify(self, op: ldap.ModifyRequest, controls=None) -> typing.Iterable[ldap.ProtocolOp]:
+		self.logger.info('MODIFY %s', op)
 		reject_critical_controls(controls)
 		raise exceptions.LDAPInsufficientAccessRights()
 
 	def handle_add(self, op: ldap.AddRequest, controls=None) -> typing.Iterable[ldap.ProtocolOp]:
+		self.logger.info('ADD %s', op)
 		reject_critical_controls(controls)
 		raise exceptions.LDAPInsufficientAccessRights()
 
 	def handle_delete(self, op: ldap.DelRequest, controls=None) -> typing.Iterable[ldap.ProtocolOp]:
+		self.logger.info('DELETE %s', op)
 		reject_critical_controls(controls)
 		raise exceptions.LDAPInsufficientAccessRights()
 
 	def handle_modifydn(self, op: ldap.ModifyDNRequest, controls=None) -> typing.Iterable[ldap.ProtocolOp]:
+		self.logger.info('MODIFYDN %s', op)
 		reject_critical_controls(controls)
 		raise exceptions.LDAPInsufficientAccessRights()
 
 	def handle_compare(self, op: ldap.CompareRequest, controls=None) -> typing.Iterable[ldap.ProtocolOp]:
+		self.logger.info('COMPRAE %s', op)
 		reject_critical_controls(controls)
 		raise exceptions.LDAPInsufficientAccessRights()
 
 	def handle_abandon(self, op: ldap.AbandonRequest, controls=None) -> typing.NoReturn:
+		self.logger.info('ABANDON %s', op)
 		reject_critical_controls(controls)
 
 	def handle_extended(self, op: ldap.ExtendedRequest, controls=None) -> typing.Iterable[ldap.ProtocolOp]:
+		self.logger.info('EXTENDED %s', op)
 		reject_critical_controls(controls)
 		raise exceptions.LDAPProtocolError()
 
@@ -395,11 +419,21 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 		raise exceptions.LDAPAuthMethodNotSupported()
 
 	def handle_search(self, op, controls=None):
+		self.logger.info('SEARCH request dn=%r dn_scope=%s filter=%r, attributes=%r',
+		                 op.baseObject, op.scope.name, op.filter.get_filter_string(),
+		                 ' '.join(op.attributes))
 		reject_critical_controls(controls)
+		entries = 0
+		time_start = time.perf_counter()
 		for obj in self.do_search(op.baseObject, op.scope, op.filter):
 			if obj.match_search(op.baseObject, op.scope, op.filter):
-				yield obj.get_search_result_entry(op.attributes, op.typesOnly)
+				entry = obj.get_search_result_entry(op.attributes, op.typesOnly)
+				self.logger.debug('SEARCH entry %r', entry)
+				entries += 1
+				yield entry
 		yield ldap.SearchResultDone(ldap.LDAPResultCode.success)
+		time_end = time.perf_counter()
+		self.logger.info('SEARCH done entries=%d duration_seconds=%.3f', entries, time_end - time_start)
 
 	def do_search(self, baseobj, scope, filterobj):
 		'''Do LDAP SEARCH operation
@@ -423,12 +457,15 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 		yield from self.static_objects
 
 	def handle_compare(self, op, controls=None):
+		self.logger.info('COMPRAE request "%s" %s=%s', op.entry, op.ava.attributeDesc, repr(op.ava.assertionValue))
 		obj = self.do_compare(op.entry, op.ava.attributeDesc, op.ava.assertionValue)
 		if obj is None:
 			raise exceptions.LDAPNoSuchObject()
 		if obj.match_compare(op.ava.attributeDesc, op.ava.assertionValue):
+			self.logger.info('COMPRAE result TRUE')
 			return [ldap.CompareResponse(ldap.LDAPResultCode.compareTrue)]
 		else:
+			self.logger.info('COMPRAE result FALSE')
 			return [ldap.CompareResponse(ldap.LDAPResultCode.compareFalse)]
 
 	def do_compare(self, dn, attribute, value):
@@ -455,6 +492,7 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 		return None
 
 	def handle_unbind(self, op, controls=None):
+		self.logger.info('UNBIND')
 		reject_critical_controls(controls)
 		self.keep_running = False
 		return []
@@ -462,6 +500,7 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 	def handle_extended(self, op, controls=None):
 		reject_critical_controls(controls)
 		if op.requestName == ldap.EXT_STARTTLS_OID and self.supports_starttls:
+			self.logger.info('EXTENDED STARTTLS')
 			# StartTLS (RFC 4511)
 			yield ldap.ExtendedResponse(ldap.LDAPResultCode.success, responseName=ldap.EXT_STARTTLS_OID)
 			try:
@@ -470,10 +509,12 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 				traceback.print_exc()
 				self.keep_running = False
 		elif op.requestName == ldap.EXT_WHOAMI_OID and self.supports_whoami:
+			self.logger.info('EXTENDED WHOAMI')
 			# "Who am I?" Operation (RFC 4532)
 			identity = (self.do_whoami() or '').encode()
 			yield ldap.ExtendedResponse(ldap.LDAPResultCode.success, responseValue=identity)
 		elif op.requestName == ldap.EXT_PASSWORD_MODIFY_OID and self.supports_password_modify:
+			self.logger.info('EXTENDED PASSWORD_MODIFY')
 			# Password Modify Extended Operation (RFC 3062)
 			newpw = None
 			if op.requestValue is None:
@@ -488,6 +529,7 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 				encoded = ldap.PasswdModifyResponseValue.to_ber(ldap.PasswdModifyResponseValue(newpw))
 				yield ldap.ExtendedResponse(ldap.LDAPResultCode.success, responseValue=encoded)
 		else:
+			self.logger.warning('Unsupported or disabled EXTENDED operation %r', op.requestName)
 			yield from super().handle_extended(op, controls) # pylint: disable=not-an-iterable
 
 	#: :any:`ssl.SSLContext` for StartTLS