Buscar

Limpiar filtro
Artículo
Ricardo Paiva · 29 nov, 2021

Cómo transferir archivos a través de REST para almacenar en una propiedad. Parte 2

En la primera parte de esta serie de artículos, hablamos sobre cómo leer un fragmento "grande" de datos del contenido sin procesar de un método HTTP POST y guardarlo en una base de datos como una propiedad de flujo de una clase. Ahora veremos cómo guardar esos datos y metadatos en formato JSON. Desafortunadamente, Advanced REST Client no permite configurar objetos JSON con datos binarios como valor de una clave (o quizá simplemente no he descubierto cómo hacerlo), así que decidí escribir un cliente simple en ObjectScript para enviar datos al servidor. Creé una nueva clase llamada RestTransfer.Client y le añadí los parámetros Server = "localhost" y Port = 52773 para describir mi servidor web. Y creé un método de clases GetLink en el que creo una nueva instancia de la clase %Net.HttpRequest y establezco sus propiedades con los parámetros mencionados anteriormente. Para enviar la solicitud POST al servidor, creé un método de clase SendFileDirect, que lee el archivo que quiero enviar al servidor y escribe su contenido en la propiedad EntityBody de mi solicitud. Después de esto, llamo al método Post("/RestTransfer/file") y, si se completa correctamente, la respuesta estará en la propiedad HttpResponse. Para ver el resultado devuelto por el servidor, llamo al método OutputToDevice de la respuesta. Este es el método de la clase: Class RestTransfer.Client { Parameter Server = "localhost"; Parameter Port = 52773; Parameter Https = 0; ClassMethod GetLink() As %Net.HttpRequest { set request = ##class(%Net.HttpRequest).%New() set request.Server = ..#Server set request.Port = ..#Port set request.ContentType = "application/octet-stream" quit request } ClassMethod SendFileDirect(aFileName) As %Status { set sc = $$$OK set request = ..GetLink() set s = ##class(%Stream.FileBinary).%New() set s.Filename = aFileName While 's.AtEnd { do request.EntityBody.Write(s.Read(.len, .sc)) Quit:$System.Status.IsError(sc) } Quit:$System.Status.IsError(sc) set sc = request.Post("/RestTransfer/file") Quit:$System.Status.IsError(sc) set response=request.HttpResponse do response.OutputToDevice() Quit sc } } Podemos llamar a este método para transferir los mismos archivos que se utilizaron en el artículo anterior: do ##class(RestTransfer.Client).SendFileDirect("D:\Downloads \2020_1012_114732_020.JPG") do ##class(RestTransfer.Client).SendFileDirect("D:\Downloads\Archive.xml") do ##class(RestTransfer.Client).SendFileDirect("D:\Downloads\arc-setup.exe") Observaremos las respuestas del servidor para cada archivo: HTTP/1.1 200 OK CACHE-CONTROL: no-cache CONTENT-LENGTH: 15 CONTENT-TYPE: text/html; charset=utf-8 DATE: Thu, 05 Nov 2020 15:13:23 GMT EXPIRES: Thu, 29 Oct 1998 17:04:19 GMT PRAGMA: no-cache SERVER: Apache {"Status":"OK"} Y tendremos los mismos datos en los globals que antes: Esto significa que nuestro cliente funciona y ahora lo podemos expandir para enviar JSON al servidor. Introducción a JSON JSON es un formato ligero para almacenar y transportar datos en el que los datos están en pares nombre/valor separados por comas. En este caso, el JSON será algo parecido a esto: { "Name": "test.txt", "File": "Hello, world!" } IRIS tiene varias clases que permiten trabajar con JSON. Yo utilizaré la siguientes: %JSON.Adaptor ofrece una forma de serializar un objeto habilitado para JSON como un documento JSON y viceversa. %Library.DynamicObject ofrece una forma de ensamblar de forma dinámica datos que se pueden transferir cómodamente entre un cliente web y un servidor. Puedes encontrar más información sobre estas y otras clases que te permiten trabajar con JSON en la sección Class Reference de la documentación y en las guías para desarrollar aplicaciones. La principal ventaja de estas clases es que ofrecen una forma sencilla de crear JSON a partir de un objeto IRIS o de crear un objeto a partir de JSON. Voy a mostrar dos enfoques sobre cómo se puede trabajar con JSON para enviar/recibir archivos, en concreto uno que utiliza %Library.DynamicObject para crear un objeto en el lado del cliente y serializarlo en un documento JSON y %JSON.Adaptor para deserializarlo de nuevo a RestTransfer.FileDesc en el lado del servidor, y otro en el que desarrollo de forma manual una cadena para enviar y analizar en el lado del servidor. Para que sea más legible, crearé dos métodos diferentes en el servidor para cada enfoque. Por ahora, nos centraremos en la primera. Cómo crear JSON con IRIS Para empezar, debemos heredar la clase RestTransfer.FileDesc de %JSON.Adaptor. Esto nos permite utilizar el método de instancia %JSONImport() para deserializar un documento JSON directamente en un objeto. Ahora, esta clase tendrá el siguiente aspecto: Class RestTransfer.FileDesc Extends (%Persistent, %JSON.Adaptor) { Property File As %Stream.GlobalBinary; Property Name As %String; } Añadamos una nueva ruta a UrlMap en el broker de la clase: <Route Url="/jsons" Method="POST" Call="InsertJSONSmall"/> Esto especifica que, cuando el servicio recibe un comando POST con la URL /RestTransfer/jsons, debería llamar al método de clase InsertJSONSmall. Este método espera recibir un texto en formato JSON con dos pares clave-valor donde los contenidos del fichero serán inferiores a la longitud máxima de una cadena. Definiré las propiedades Name y File del objeto, lo guardaré en la base de datos y devolveré el estado y un mensaje con formato JSON que indicará si es correcto o es un error. Este es el método de la clase. ClassMethod InsertJSONSmall() As %Status { Set result={} Set st=0 set f = ##class(RestTransfer.FileDesc).%New() if (f = $$$NULLOREF) { do result.%Set("Message", "Couldn't create an instance of class") } else { set st = f.%JSONImport(%request.Content) If $$$ISOK(st) { set st = f.%Save() If $$$ISOK(st) { do result.%Set("Status","OK") } else { do result.%Set("Message",$system.Status.GetOneErrorText(st)) } } else { do result.%Set("Message",$system.Status.GetOneErrorText(st)) } } write result.%ToJSON() Quit st } ¿Qué hace este método? Recibe la propiedad Content del objeto %request y, usando el método %JSONImport heredado de la clase %JSON.Adaptor, la convierte en un objeto RestTransfer.FileDesc. Si se producen problemas durante la conversión, configuramos un JSON con la descripción del error: {"Message",$system.Status.GetOneErrorText(st)} De lo contrario, guardamos el objeto. Si se guarda sin problemas, creamos un mensaje JSON {"Status","OK"}. En caso contrario, un mensaje JSON con la descripción del error. Finalmente, escribimos el código JSON en una respuesta y devolvemos el estado st. En el lado del cliente añadí un nuevo método de clase SendFile para enviar archivos al servidor. El código es muy similar al de SendFileDirect pero, en vez de escribir los contenidos del archivo directamente en la propiedad EntityBody, creo una nueva instancia de la clase %Library.DynamicObject, establezco su propiedad Name igual al nombre del archivo, y codifico y copio los contenidos del archivo en la propiedad File. Para codificar los contenidos del archivo utilizo el método Base64EncodeStream() propuesto por Vitaliy Serdtsev. Después, usando el método %ToJSON de la clase %Library.DynamicObject, serializo mi objeto en un documento JSON, lo escribo en el contenido de la solicitud y llamo al método Post("/RestTransfer/jsons"). Este es el método de la clase: ClassMethod SendFile(aFileName) As %Status { Set sc = $$$OK Set request = ..GetLink() set s = ##class(%Stream.FileBinary).%New() set s.Filename = aFileName set p = {} set p.Name = s.Filename set sc = ..Base64EncodeStream(s, .t) Quit:$System.Status.IsError(sc) While 't.AtEnd { set p.File = p.File_t.Read(.len, .sc) Quit:$System.Status.IsError(sc) } do p.%ToJSON(request.EntityBody) Quit:$System.Status.IsError(sc) set sc = request.Post("/RestTransfer/jsons") Quit:$System.Status.IsError(sc) Set response=request.HttpResponse do response.OutputToDevice() Quit sc } Llamamos a este método y al método SendFileDirect para transferir varios archivos pequeños: do ##class(RestTransfer.Client).SendFile("D:\Downloads\Outline Template.pdf") do ##class(RestTransfer.Client).SendFileDirect("D:\Downloads\Outline Template.pdf") do ##class(RestTransfer.Client).SendFile("D:\Downloads\pic3.png") do ##class(RestTransfer.Client).SendFileDirect("D:\Downloads\pic3.png") do ##class(RestTransfer.Client).SendFile("D:\Downloads\Archive (1).xml") do ##class(RestTransfer.Client).SendFileDirect("D:\Downloads\Archive (1).xml") Obtendremos los siguientes resultados: Como puedes observar, las longitudes son las mismas y, si guardamos esos archivos en un disco duro, veremos que no han cambiado. Configuración manual de JSON Ahora nos centraremos en el segundo enfoque, que consiste en configurar el JSON manualmente. Para hacerlo, añadiremos una nueva ruta a UrlMap en el broker de la clase: <Route Url="/json" Method="POST" Call="InsertJSON"/> Esto especifica que cuando el servicio recibe un comando POST con la URL /RestTransfer/json, debería llamar al método de clase InsertJSON. En este método, espero recibir el mismo JSON, pero no impongo ningún límite a la longitud del archivo. Este es el método de la clase: ClassMethod InsertJSON() As %Status { Set result={} Set st=0 set t = ##class(%Stream.TmpBinary).%New() While '%request.Content.AtEnd { set len = 32000 set temp = %request.Content.Read(.len, .sc) set:len<32000 temp = $extract(temp,1,*-2) set st = t.Write($ZCONVERT(temp, "I", "RAW")) } do t.Rewind() set f = ##class(RestTransfer.FileDesc).%New() if (f = $$$NULLOREF) { do result.%Set("Message", "Couldn't create an instance of class") } else { set str = t.Read() set pos = $LOCATE(str,""",") set f.Name = $extract(str, 10, pos-1) do f.File.Write($extract(str, pos+11, *)) While 't.AtEnd { do f.File.Write(t.Read(.len, .sc)) } If $$$ISOK(st) { set st = f.%Save() If $$$ISOK(st) { do result.%Set("Status","OK") } else { do result.%Set("Message",$system.Status.GetOneErrorText(st)) } } else { do result.%Set("Message",$system.Status.GetOneErrorText(st)) } } write result.%ToJSON() Quit st } ¿Qué hace este método? Primero, creo una nueva instancia de un flujo temporal, %Stream.TmpBinary, y copio en él el contenido de la solicitud. Como voy a trabajar con ella como una cadena, necesito deshacerme de las comillas finales (") y de las llaves (}). Para hacerlo, en el último fragmento del flujo dejo fuera los dos últimos caracteres: $extract(temp,1,*-2). A la vez, convierto mi cadena usando la tabla de traducción "RAW": $ZCONVERT(temp, "I", "RAW"). Después creo una nueva instancia de mi clase RestTransfer.FileDesc y hago las mismas verificaciones que en los otros métodos. Ya conozco la estructura de mi cadena, así que extraigo el nombre del archivo y el archivo en sí y las defino en las propiedades correspondientes. En el lado del cliente, modifiqué mi método de clase SendFile y, antes de crear el JSON, verifico la longitud del archivo. Si es inferior a los 2 000 000 de bytes (límite aparente del método %JSONImport), llamo a Post("/RestTransfer/jsons"). De lo contrario, llamo a Post("/RestTransfer/json"). Ahora el método se ve así: ClassMethod SendFile(aFileName) As %Status { Set sc = $$$OK Set request = ..GetLink() set s = ##class(%Stream.FileBinary).%New() set s.Filename = aFileName if s.Size > 2000000 //3641144 max length of the string in IRIS { do request.EntityBody.Write("{""Name"":"""_s.Filename_""", ""File"":""") While 's.AtEnd { set temp = s.Read(.len, .sc) do request.EntityBody.Write($ZCONVERT(temp, "O", "RAW")) Quit:$System.Status.IsError(sc) } do request.EntityBody.Write("""}") set sc = request.Post("/RestTransfer/json") } else { set p = {} set p.Name = s.Filename set sc = ..Base64EncodeStream(s, .t) Quit:$System.Status.IsError(sc) While 't.AtEnd { set p.File = p.File_t.Read(.len, .sc) Quit:$System.Status.IsError(sc) } do p.%ToJSON(request.EntityBody) Quit:$System.Status.IsError(sc) set sc = request.Post("/RestTransfer/jsons") } Quit:$System.Status.IsError(sc) Set response=request.HttpResponse do response.OutputToDevice() Quit sc } Para llamar al segundo método, creo la cadena con JSON de forma manual. Y para transferir el archivo en sí, en el lado del cliente también convierto los contenidos mediante la tabla de traducción "RAW": ($ZCONVERT(temp, "O", "RAW"). Ahora llamamos a este método y al método SendFileDirect para transferir varios archivos de diferentes tamaños: do ##class(RestTransfer.Client).SendFile(“D:\Downloads\pic3.png”) do ##class(RestTransfer.Client).SendFileDirect(“D:\Downloads\pic3.png”) do ##class(RestTransfer.Client).SendFile(“D:\Downloads\Archive (1).xml”) do ##class(RestTransfer.Client).SendFileDirect(“D:\Downloads\Archive (1).xml”) do ##class(RestTransfer.Client).SendFile(“D:\Downloads\Imagine Dragons-Thunder.mp3”) do ##class(RestTransfer.Client).SendFileDirect(“D:\Downloads\Imagine Dragons-Thunder.mp3”) do ##class(RestTransfer.Client).SendFile(“D:\Downloads\ffmpeg-win-2.2.2.exe”) do ##class(RestTransfer.Client).SendFileDirect(“D:\Downloads\ffmpeg-win-2.2.2.exe”) Obtendremos los siguientes resultados: Podremos ver que las longitudes son las mismas por lo que todo funciona como se supone. Conclusión De nuevo, puedes leer más sobre la creación de servicios REST en la documentación. El código de ejemplo para ambos enfoques se encuentra en GitHub y en InterSystems Open Exchange. Sobre la transferencia de resultados del módulo TWAIN que se comenta en el primer artículo de esta serie, depende del tamaño de los datos que recibas. Si la máxima resolución será limitada y los archivos serán pequeños, puedes utilizar las clases del sistema %JSON.Adaptor y %Library.DynamicObject. Pero para estar seguros, es mejor enviar un archivo directamente al cuerpo de un comando POST o configurar el JSON manualmente. También puede ser una buena idea utilizar solicitudes que abarquen varios archivos y dividirlos en varias partes, enviarlos por separado y volver a consolidarlos en un solo archivo en el lado del servidor. En cualquier caso, si tienes alguna pregunta o sugerencia, puedes dejarla en la sección de comentarios.
Pregunta
Yone Moreno · 30 mar, 2022

DICOM: ¿ Obtenemos respuesta "corta" al Simular ser Sistema Origen desde línea de comando hacia Servicio TCP de DICOM, con respecto a un Servicio de Studio clásico de DICOM?

Buenos días, Primero, ante todo, muchas gracias de antemano por leernos y responder Además, agradecer cualquier apoyo, porque es un alivio, apoyo, aporte, auxilio contar con personas con más entendimiento, conocimiento y práctica. Existe la siguiente necesidad: Se dispone de 2 circuitos: 1º Circuito DICOM de "Studio" ( Servicio clásico ) Servicio: clase: DICOM.BS.QueryService Proceso: clase: DICOM.BP.QueryProcess Operacion: clase: EnsLib.DICOM.Operation.TCP Probamos desde la "Salida" del "Studio" mediante: do ##class(DICOM.BS.QueryService).TestFind("102030") Donde "102030" es el PatientID del usuario Lo interesante aquí es la traza: A nivel visual: Lo importante, enviamos en la petición a Destino en el DICOM.Document: Recibimos bastante contenido: Sin embargo, mediante el otro circuito hay discrepancias: 2º Circuito DICOM de TCP( Se prueba por línea de comandos ) Servicio: clase: EnsLib.DICOM.Service.TCP Proceso: clase: DICOM.BP.QueryProcess Operacion: clase: EnsLib.DICOM.Operation.TCP Para probar ejecutamos: ./findscu -b VNAPRE -c ESBPRE@10.XWZ.4.ABC:19XYZ -m PatientID="102030" La traza visual: Siendo lo enviado, a sistema destino, mediante DICOM.Document: Aquí viene la cuestión: ¿ por qué sale muy poca informacion en la respuesta del Destino ? ¿ por qué sale muy poca informacion en la respuesta del Destino ? En particular la traza a nivel de línea de comandos: $ ./findscu -b VNAPRE -c ESBPRE@10.[IP_Destino]:19586 -m PatientID="102030" 10:30:38.820 INFO - Initiate connection from 0.0.0.0/0.0.0.0:0 to 10.[IP_Destino]:19586 10:30:38.867 INFO - Established connection Socket[addr=/10.[IP_Destino],port=19586,localport=52515] 10:30:38.888 DEBUG - /10.[IP_Origen]:52515->/10.[IP_Destino]:19586(1): enter state: Sta4 - Awaiting transport connection opening to complete 10:30:38.890 INFO - VNAPRE->ESBPRE(1) << A-ASSOCIATE-RQ 10:30:38.890 DEBUG - A-ASSOCIATE-RQ[ calledAET: ESBPRE callingAET: VNAPRE applicationContext: 1.2.840.10008.3.1.1.1 - DICOM Application Context Name implClassUID: 1.2.40.0.13.1.3 implVersionName: null maxPDULength: 16378 maxOpsInvoked/maxOpsPerformed: 0/0 PresentationContext[id: 1 as: 1.2.840.10008.5.1.4.1.2.2.1 - Study Root Query/Retrieve Information Model - FIND ts: 1.2.840.10008.1.2 - Implicit VR Little Endian ts: 1.2.840.10008.1.2.1 - Explicit VR Little Endian ts: 1.2.840.10008.1.2.2 - Explicit VR Big Endian (Retired) ] ] 10:30:38.923 DEBUG - VNAPRE->ESBPRE(1): enter state: Sta5 - Awaiting A-ASSOCIATE-AC or A-ASSOCIATE-RJ PDU 10:30:38.936 INFO - VNAPRE->ESBPRE(1) >> A-ASSOCIATE-AC 10:30:38.937 DEBUG - A-ASSOCIATE-AC[ calledAET: ESBPRE callingAET: VNAPRE applicationContext: 1.2.840.10008.3.1.1.1 - DICOM Application Context Name implClassUID: 1.2.840.114475.1 implVersionName: ENSDICOM maxPDULength: 16378 maxOpsInvoked/maxOpsPerformed: 1/1 PresentationContext[id: 1 result: 0 - acceptance ts: 1.2.840.10008.1.2 - Implicit VR Little Endian ] ] 10:30:38.938 DEBUG - VNAPRE->ESBPRE(1): enter state: Sta6 - Association established and ready for data transfer 10:30:38.946 INFO - VNAPRE->ESBPRE(1) << 1:C-FIND-RQ[pcid=1, prior=0 cuid=1.2.840.10008.5.1.4.1.2.2.1 - Study Root Query/Retrieve Information Model - FIND tsuid=1.2.840.10008.1.2 - Implicit VR Little Endian] 10:30:38.947 DEBUG - VNAPRE->ESBPRE(1) << 1:C-FIND-RQ Command: (0000,0002) UI [1.2.840.10008.5.1.4.1.2.2.1] AffectedSOPClassUID (0000,0100) US [32] CommandField (0000,0110) US [1] MessageID (0000,0700) US [0] Priority (0000,0800) US [0] CommandDataSetType 10:30:39.006 DEBUG - VNAPRE->ESBPRE(1) << 1:C-FIND-RQ Dataset: (0008,0052) CS [STUDY] QueryRetrieveLevel (0010,0020) LO [102030] PatientID 10:30:39.170 INFO - VNAPRE->ESBPRE(1) >> 1:C-FIND-RSP[pcid=1, status=ff00H cuid=1.2.840.10008.5.1.4.1.2.2.1 - Study Root Query/Retrieve Information Model - FIND tsuid=1.2.840.10008.1.2 - Implicit VR Little Endian] 10:30:39.171 DEBUG - VNAPRE->ESBPRE(1) >> 1:C-FIND-RSP Command: (0000,0002) UI [1.2.840.10008.5.1.4.1.2.2.1] AffectedSOPClassUID (0000,0100) US [32800] CommandField (0000,0120) US [1] MessageIDBeingRespondedTo (0000,0800) US [65278] CommandDataSetType (0000,0900) US [65280] Status 10:30:39.173 DEBUG - VNAPRE->ESBPRE(1) >> 1:C-FIND-RSP Dataset: (0008,0052) CS [STUDY] QueryRetrieveLevel (0008,0054) AE [VNAPRE] RetrieveAETitle (0010,0020) LO [102030] PatientID 10:30:39.175 INFO - VNAPRE->ESBPRE(1) >> 1:C-FIND-RSP[pcid=1, status=ff00H cuid=1.2.840.10008.5.1.4.1.2.2.1 - Study Root Query/Retrieve Information Model - FIND tsuid=1.2.840.10008.1.2 - Implicit VR Little Endian] 10:30:39.175 DEBUG - VNAPRE->ESBPRE(1) >> 1:C-FIND-RSP Command: (0000,0002) UI [1.2.840.10008.5.1.4.1.2.2.1] AffectedSOPClassUID (0000,0100) US [32800] CommandField (0000,0120) US [1] MessageIDBeingRespondedTo (0000,0800) US [65278] CommandDataSetType (0000,0900) US [65280] Status 10:30:39.176 DEBUG - VNAPRE->ESBPRE(1) >> 1:C-FIND-RSP Dataset: (0008,0052) CS [STUDY] QueryRetrieveLevel (0008,0054) AE [VNAPRE] RetrieveAETitle (0010,0020) LO [102030] PatientID 10:30:39.176 INFO - VNAPRE->ESBPRE(1) >> 1:C-FIND-RSP[pcid=1, status=ff00H cuid=1.2.840.10008.5.1.4.1.2.2.1 - Study Root Query/Retrieve Information Model - FIND tsuid=1.2.840.10008.1.2 - Implicit VR Little Endian] 10:30:39.176 DEBUG - VNAPRE->ESBPRE(1) >> 1:C-FIND-RSP Command: (0000,0002) UI [1.2.840.10008.5.1.4.1.2.2.1] AffectedSOPClassUID (0000,0100) US [32800] CommandField (0000,0120) US [1] MessageIDBeingRespondedTo (0000,0800) US [65278] CommandDataSetType (0000,0900) US [65280] Status 10:30:39.177 DEBUG - VNAPRE->ESBPRE(1) >> 1:C-FIND-RSP Dataset: (0008,0052) CS [STUDY] QueryRetrieveLevel (0008,0054) AE [VNAPRE] RetrieveAETitle (0010,0020) LO [102030] PatientID Observamos que existen 3 respuestas: 10:30:39.170 INFO - VNAPRE->ESBPRE(1) >> 1:C-FIND-RSP 10:30:39.175 INFO - VNAPRE->ESBPRE(1) >> 1:C-FIND-RSP 10:30:39.176 INFO - VNAPRE->ESBPRE(1) >> 1:C-FIND-RSP Sin embargo ¿ por qué sale muy poca informacion en la respuesta del Destino ? En concreto, en el circuito 1º obtenemos: SpecificCharacterSet, StudyDate, StudyDescription, PatientName, StudyInstanceUID Mientras que en el TCP de DICOM, en el 2º circuito: Unicamente: QueryRetrieveLevel, RetrieveAETitle, PatientID ¿Ustedes nos podrían, por favor, indicar documentación, ejemplos de código, trazas visuales, o referencias de cualquier tipo que nos aporten para depurar? Muchas gracias de antemano por su tiempo, al leer y responder Un saludo Hola Yone, Imagino que habrá diferencias entre el C-FIND que se envía en el primer caso y el segundo. En el primer caso, el que retorna más resultados, entiendo que es el caso del ejemplo. Revisa cómo se está generando exactamente ese C-FIND: https://github.com/intersystems-ib/iris-dicom-sample/blob/7bc3c00dfa1bbbe9b0a711df2f344a15dee09be1/iris/src/DICOM/%20Msg/QueryReq.cls#L27 En principio, se está haciendo un PatientRootQuery (tAffectedSOPClassUID). No sé si puede servirte para algo, pero en el herramienta findscu creo que hay una opción para especificar el Information Model a utilizar: https://github.com/dcm4che/dcm4che/blob/master/dcm4che-tool/dcm4che-tool-findscu/README.md -M <name> specifies Information Model. Supported names: PatientRoot, StudyRoot, PatientStudyOnly, MWL, UPSPull, UPSWatch, UPSQuery, HangingProtocol or ColorPalette. If no Information Model is specified, StudyRoot will be used. Es de agradecer tu respuesta Alberto Son un apoyo tus explicaciones, ejemplos y enlaces Los revisamos y te responderíamos Es de agradecer, Alberto, tu respuesta, y sobre todo las explicaciones y enlaces A continuación te detallamos la situación, Alberto, para que ustedes nos indiquen, ordenen, recomienden, sugieran vías o formas de continuar: A continuación, se muestran las trazas del WireShark Primero observamos la traza Simulada desde Línea de Comandos mediante: ./findscu -b VNAPRE -c ESBPRE@10.136.4.142:19586 -m PatientID="102030" -M StudyRoot Segundo se muestra la traza Real desde Sistema Origen hacia Sistema Destino: La principal diferencia radica en la petición, la cual está en 1 único paquete en la Simulada, el cual está titulado como: 1072 … P-DATA, C-FIND-RQ ID=1, C-FIND-RQ-DATA Sin embargo, en la real, la petición consta de 2 paquetes: 1904 … P-DATA, C-FIND-RQ ID=1 1906 … P-DATA, C-FIND-RQ-DATA Ahondando en el detalle, comparamos la primera de las peticiones titulada como: “P-DATA, C-FIND-RQ ID=1” En la Simulada se observa lo siguiente: Donde cabe destacar estos 2 contenidos del conjunto de comandos (Command, Last Fragment): … (0000,0700) 2 Priority 0 (0000,0800) 2 Command Data Set Type 0 Los cuales en la Real son distintos: … (0000,0700) 2 Priority 2 (0000,0800) 2 Command Data Set Type 1 Además, también es importante recalcar que en la segunda petición reseñada como “P-DATA, C-FIND-RQ-DATA” existen diferencias: En la Real es más completa: PDV, C-FIND-RQ-DATA PDV Length: 130 Context: 0x01 (Implicit VR Little Endian: Default Transfer Syntax for DICOM, Study Root Query/Retrieve Information Model - FIND) Flags: 0x02 (Data, Last Fragment) (0008,0005) 0 Specific Character Set <Empty> (0008,0020) 0 Study Date <Empty> (0008,0030) 0 Study Time <Empty> (0008,0050) 0 Accession Number <Empty> (0008,0052) 6 Query/Retrieve Level STUDY (0008,0061) 0 Modalities in Study <Empty> (0008,0090) 0 Referring Physician's Name <Empty> (0008,1010) 0 Station Name <Empty> (0008,1030) 0 Study Description <Empty> (0010,0010) 0 Patient's Name <Empty> (0010,0020) 2 Patient ID 9 (0010,0030) 0 Patient's Birth Date <Empty> (0010,0040) 0 Patient's Sex <Empty> (0020,000d) 0 Study Instance UID <Empty> (0020,0010) 0 Study ID <Empty> Sin embargo, en la Simulada únicamente existen 2 datos: PDV, C-FIND-RQ-DATA PDV Length: 30 Context: 0x01 (Implicit VR Little Endian: Default Transfer Syntax for DICOM, Study Root Query/Retrieve Information Model - FIND) Flags: 0x02 (Data, Last Fragment) (0008,0052) 6 Query/Retrieve Level STUDY (0010,0020) 6 Patient ID 102030 Por otro lado, lo que nos genera mayor inquietud, nos pone en vilo, y es extraño, resulta ser las discrepacias entre los resultados de los estudios médicos obtenidos mediante servicio ‘interno’ y mediante ‘simulación por herramienta externa’ hacia el Servicio TCP de DICOM En concreto cuando ejecutamos desde la “Salida” del “Studio” de ESBPRE lo siguiente: do ##class(DICOM.BS.QueryService).TestFind("102030") Se envía la petición: Y para cada uno de los 3 estudios médicos respondidos por el Sistema Destino, obtenemos datos completos: Mientras que cuando simulamos ser Sistema Origen, gracias a la herramienta ‘findscu’ de Línea de Comandos, ejecutamos: ./findscu -b VNAPRE -c ESBPRE@10.136.4.142:19586 -m PatientID="102030" -M StudyRoot Generándose la petición siguiente en la traza: Si prestamos atención a la imagen anterior, se visualiza que únicamente se remiten 2 filas en el “DataSet”, las cuales corresponden a: ‘QueryRetrieveLevel’ y ‘PatientID’ Siendo la respuesta por parte del Sistema Origen con 3 estudios médicos, donde únicamente se incluyen 3 filas en el DataSet: Las cuales corresponden al: ‘QueryRetrieveLevel’, ‘RetrieveAETitle’ y ‘PatientID’ De esta forma observamos grandes diferencias entre la respuesta generada por el Sistema Destino, cuando enviamos desde nuestro servicio interno, frente a cuando remitimos la consulta simulando ser sistema origen por TCP de DICOM ¿ustedes cómo recomiendan continuar? Muchas gracias, Alberto, por leer, responder, y tomar su tiempo para atendernos Gracias a ustedes por indicarnos, ordenarnos, recomendarnos, sugerirnos, técnicas, documentación, ejemplos; mediante los cuales investigar, depurar, indagar esta cuestión Un saludo
Artículo
Kurro Lopez · 25 nov, 2019

Los Globals son espadas mágicas para almacenar datos. Parte 2. Árboles

Principiantes- ver Parte 1. 3. Variantes de estructuras cuando se usan globals Una estructura, como un árbol ordenado, tiene varios casos especiales. Echemos un vistazo a aquellos que tienen un valor práctico para trabajar con globals. 3.1 Caso especial 1. Un nodo sin ramas Los globals pueden usarse no solo como una matriz, sino como variables regulares. Por ejemplo, para crear un contador: Set ^counter = 0 ; setting counter Set id=$Increment(^counter) ; atomic incrementation Al mismo tiempo, un global puede tener ramas adicionales además de su valor. Uno no excluye al otro. 3.2 Caso especial 2. Un nodo y múltiples ramas De hecho, es una base clásica de valor clave. Y si guardamos tuplas de valores en lugar de valores, obtendremos una tabla regular con una clave primaria. Para implementar una tabla basada en globals, tendremos que formar cadenas de valores de columna, luego guardarlas en un global por la clave primaria. Para poder dividir la cadena en columnas durante la lectura, podemos usar lo siguiente: Caracteres delimitadores Set ^t(id1) = "col11/col21/col31" Set ^t(id2) = "col12/col22/col32" Un esquema fijo, por el cual cada campo ocupa un número particular de bytes. Así es como generalmente se hace en bases de datos relacionales. Una función especial $LB (introducida en Caché) que compone una cadena de valores. Set ^t(id1) = $LB("col11", "col21", "col31") Set ^t(id2) = $LB("col12", "col22", "col32") Lo interesante es que no es difícil hacer algo similar a las claves foráneas en bases de datos relacionales que usan globals. Llamemos a tales estructuras index globals. Un índice global es un árbol suplementario para la búsqueda rápida por campos que no son una parte integral de la clave primaria del global principal. Necesita escribir código adicional para llenarlo y usarlo. Creemos un índice global basado en la primera columna. Set ^i("col11", id1) = 1 Set ^i("col12", id2) = 1 Para buscar rápidamente por la primera columna, deberá buscar en ^i global y encontrar las claves principales (id) correspondientes al valor necesario en la primera columna. Al insertar un valor, podemos crear valores e índices globals para los campos necesarios. Para mayor fiabilidad, envuélvala en una transacción. TSTART Set ^t(id1) = $LB("col11", "col21", "col31") Set ^i("col11", id1) = 1 TCOMMIT Más información sobre creando tablas en M usando globals y emulación de claves secundarias. Estas tablas funcionarán tan rápido como en las bases de datos tradicionales (o incluso más rápido) si las funciones de inserción/actualización/eliminación se escriben en COS/M y se compilan. Verifiqué esta declaración aplicando una gran cantidad de operaciones INSERT y SELECT a una sola tabla de dos columnas, también usando el comando TSTART y TCOMMIT (transacciones). No he probado escenarios más complejos con acceso concurrente y transacciones paralelas. Sin usar transacciones, la velocidad de inserción para un millón de valores fue de 778.361 inserciones/seg. Para 300 millones de valores, la velocidad fue de 422.141 inserciones/segundo. Cuando se utilizaron las transacciones, la velocidad alcanzó 572.082 inserciones/segundo para 50 millones de valores. Todas las operaciones se ejecutaron desde el código M compilado. Usé discos duros normales, no SSD. RAID5 con reescritura. Todo se ejecuta en una CPU Phenom II 1100T. Para realizar la misma prueba para una base de datos SQL, necesitaríamos escribir un procedimiento almacenado que haga inserciones en un bucle. Al probar MySQL 5.5 (almacenamiento InnoDB) utilizando el mismo método, nunca obtuve más de 11K inserciones por segundo. Correcto, la implementación de tablas con globals es más compleja que hacer lo mismo en bases de datos relacionales. Es por eso que los DB industriales basados en globales tienen acceso SQL para un trabajo simplificado con datos tabulares. En general, si el esquema de datos no va a cambiar con frecuencia, la velocidad de inserción no es crítica y la base de datos completa se puede representar fácilmente con tablas normalizadas, es más fácil trabajar con SQL, ya que proporciona un mayor nivel de abstracción. En este caso, quería mostrar que los globals pueden usarse como constructores para crear otras bases de datos. Al igual que el lenguaje ensamblador que se puede usar para crear otros idiomas. Y aquí hay algunos ejemplos de uso de globals para crear contrapartes de valores-clave, listas, conjuntos, tablas, bases de datos orientadas a documentos. Si necesita crear una base de datos no estándar con un esfuerzo mínimo, debe considerar el uso de globals. 3.3 Caso especial 3. Un árbol de dos niveles con cada nodo de segundo nivel que tiene un número fijo de ramas Probablemente lo hayas adivinado: es una implementación alternativa de tablas usando globales. Comparémoslo con el anterior. Tablas en un árbol de dos niveles vs. Tablas en un árbol de un nivel Contras Pros Inserciones más lentas, ya que el número de nodos debe establecerse igual al número de columnas Mayor consumo de espacio en el disco duro, ya que los índices globals (como los índices de matriz) con nombres de columna ocupan espacio en el disco duro y se duplican para cada fila Acceso más rápido a los valores de columnas particulares, ya que no necesita analizar la cadena. Según mis pruebas, es un 11,5% más rápido para 2 columnas e incluso más rápido para más columnas. Más fácil cambiar el esquema de datos Código más fácil de leer Conclusión: No hay nada que destacar. Dado que el rendimiento es una de las ventajas clave de los globals, prácticamente no tiene sentido utilizar este enfoque, ya que es poco probable que funcione más rápido que las tablas normales en bases de datos relacionales. 3.4 Caso general. Árboles y llaves ordenadas Cualquier estructura de datos que se pueda representar como un árbol se ajusta a los globals de una manera perfecta. 3.4.1 Objetos con subobjetos Esta es el área donde se usan tradicionalmente los globals. Existen numerosas enfermedades, medicamentos, síntomas y métodos de tratamiento en el área médica. Es irracional crear una tabla con un millón de campos para cada paciente, especialmente porque el 99% de ellos estará en blanco. Imagine una base de datos SQL compuesta de las siguientes tablas: "Paciente" ~ 100.000 campos, "Medicación" 100.000 campos, "Terapia" 100.000 campos, "Complicaciones" 100.000 campos y así sucesivamente. Como alternativa, puede crear una base de datos con miles de tablas, cada una para un tipo de paciente en particular (¡y también pueden superponerse!), tratamiento, medicamentos y miles de tablas para las relaciones entre estas tablas. Los globals se ajustan a la atención médica como un guante, ya que hacen posible que cada paciente tenga un registro completo de casos, una lista de terapias, medicamentos administrados y sus efectos, todo en forma de árbol, sin desperdiciar demasiado espacio en el disco en columnas vacías, como sería el caso con las bases de datos relacionales. Globals funciona bien para bases de datos con detalles personales, cuando la tarea es acumular y sistematizar el máximo de varios datos personales sobre un cliente. Esto es especialmente importante para la salud, la banca, el marketing, el archivo y otras áreas. No hace falta decir que SQL también le permite emular un árbol usando solo varias tablas (EAV, 1,2,3,4,5,6, 7,8), pero es mucho más complejo y funciona más lento. En esencia, tendríamos que escribir un global basado en tablas y ocultar todas las rutinas relacionadas con tablas bajo una capa de abstracción. No es correcto emular una tecnología de nivel inferior (globales) con la ayuda de una de nivel superior (SQL). Es simplemente injustificado. No es un secreto que cambiar un esquema de datos en tablas gigantes (ALTER TABLE) puede llevar una cantidad considerable de tiempo. MySQL, por ejemplo, realiza la operación ALTER TABLE ADD|DROP COLUMN copiando todos los datos de la tabla anterior a la nueva (lo probé en MyISAM e InnoDB). Que puede colgar una base de datos de producción con miles de millones de registros durante días, si no semanas. Si estamos usando globals, cambiar la estructura de datos no tiene ningún coste para nosotros. Podemos agregar cualquier propiedad nueva a cualquier objeto en cualquier nivel de la jerarquía en cualquier momento dado. Los cambios que requieren el cambio de nombre de las ramas se pueden aplicar en modo de fondo con la base de datos en funcionamiento. Por lo tanto, cuando se trata de almacenar objetos con una gran cantidad de propiedades opcionales, los globals funcionan perfectamente bien. Permítame recordarle que el acceso a cualquiera de las propiedades es instantáneo, ya que en un global, todas las rutas son un árbol B. En el caso general, las bases de datos basadas en globals son un tipo de bases de datos orientadas a documentos que admiten el almacenamiento de información jerárquica. Por lo tanto, las bases de datos orientadas a documentos pueden competir eficientemente con los globals en el campo del almacenamiento de tarjetas médicas. Pero todavía no lo es. Tomemos MongoDB, por ejemplo. En este campo, pierde frente a los globales por las siguientes razones: Tamaño del documento. La unidad de almacenamiento es un texto en formato JSON (BSON, para ser exactos) con un tamaño máximo de alrededor de 16 MB. La limitación se introdujo a propósito para asegurarse de que la base de datos JSON no se vuelva demasiado lenta durante el análisis, cuando se guarda un gran documento JSON y se abordan valores de campo particulares. Se supone que este documento tiene información completa sobre un paciente. Todos sabemos cuán gruesas pueden ser las tarjetas de pacientes. Si el tamaño máximo de la tarjeta tiene un límite de 16 MB, filtra inmediatamente a los pacientes cuyas tarjetas contienen imágenes de resonancia magnética, rayos X y otros materiales. Una sola rama de un global puede tener gigabytes y petabytes de terabytes de datos. De alguna manera lo dice todo, pero déjame contarte más. El tiempo requerido para crear/cambiar/eliminar nuevas propiedades de la tarjeta del paciente. Dicha base de datos necesitaría copiar toda la tarjeta en la memoria (¡muchos datos!), Analizar los datos de BSON, agregar/cambiar/eliminar el nuevo nodo, actualizar índices, empaquetarlo todo nuevamente en BSON y guardarlo en el disco. Un global solo necesitaría abordar la propiedad necesaria y realizar la operación necesaria. Velocidad de acceso a propiedades particulares. Si el documento tiene muchas propiedades y una estructura de varios niveles, el acceso a propiedades particulares será más rápido porque cada ruta en el global es un árbol B. En BSON, deberá analizar linealmente el documento para encontrar la propiedad necesaria. 3.3.2 Matrices asociativas Las matrices asociativas (incluso con matrices anidadas) funcionan perfectamente con globals. Por ejemplo, esta matriz PHP se verá como la primera ilustración en 3.3.1. $a = array( "name" => "Vince Medvedev", "city" => "Moscow", "threatments" => array( "surgeries" => array("apedicectomy", "biopsy"), "radiation" => array("gamma", "x-rays"), "physiotherapy" => array("knee", "shoulder") ) ); 3.3.3 Documentos jerárquicos: XML, JSON También se puede almacenar fácilmente en globals y descomponerse de diferentes maneras. XML El método más fácil de descomponer XML en globals es almacenar atributos de etiqueta en nodos. Y si necesita acceso rápido a los atributos de etiqueta, podemos colocarlos en ramas separadas. <note id=5> <to>Alex</to> <from>Sveta</from> <heading>Reminder</heading> <body>Call me tomorrow!</body> </note> En COS, el código se verá así: Set ^xml("note")="id=5" Set ^xml("note","to")="Alex" Set ^xml("note","from")="Sveta" Set ^xml("note","heading")="Reminder" Set ^xml("note","body")="Call me tomorrow!" Nota: Para XML, JSON y matrices asociativas, puede encontrar una serie de métodos para mostrarlos en globales. En este caso particular, no reflejamos el orden de las etiquetas anidadas en la etiqueta "note". En el ^xml global, las etiquetas anidadas se mostrarán en orden alfabético. Para una visualización precisa del orden, puede usar el siguiente modelo, por ejemplo: JSON. El contenido de este documento JSON se muestra en la primera ilustración en la Sección 3.3.1: var document = { "name": "Vince Medvedev", "city": "Moscow", "threatments": { "surgeries": ["apedicectomy", "biopsy"], "radiation": ["gamma", "x-rays"], "physiotherapy": ["knee", "shoulder"] }, }; 3.3.4 Estructuras idénticas unidas por relaciones jerárquicas. Ejemplos: estructura de oficinas de ventas, puestos de personas en una estructura MLM. Base de datos de inicio. Puede usar una evaluación de fuerza de movimiento como el valor del índice de nodo global. En este caso, deberá seleccionar una rama con el mayor peso para determinar el mejor movimiento. En el global, todas las ramas en cada nivel se ordenarán por la fuerza del movimiento. La estructura de las oficinas de ventas, las personas en una empresa de MLM. Los nodos pueden almacenar algunos valores de almacenamiento en caché que reflejan las características de todo el subárbol. Por ejemplo, las ventas de este subárbol en particular. Podemos obtener información exacta sobre los logros de cualquier sucursal en cualquier momento. 4. Situaciones en las que merece la pena usar globals La primera columna contiene una lista de casos en los que el uso de globals le dará una ventaja considerable en términos de rendimiento; y la segunda, una lista de situaciones en las que simplificarán el desarrollo o el modelo de datos. Velocidad Conveniencia de procesamiento/presentación de datos Inserción [con clasificación automática en cada nivel], [indexación por la clave primaria] Eliminación de subárbol Objetos con muchas propiedades anidadas a las que necesita acceso individual Una estructura jerárquica con la posibilidad de atravesar ramas secundarias a partir de cualquier rama, incluso una no existente Transversal de árboles en profundidad Objetos/instancias con una gran cantidad de propiedades/instancias no requeridas [y/o anidadas] Datos sin esquema: a menudo se pueden agregar nuevas propiedades y eliminar las antiguas Necesita crear una base de datos no estándar Bases de datos de ruta y árboles de soluciones. Cuando los caminos se pueden representar convenientemente como un árbol Eliminación de estructuras jerárquicas sin usar recursividad Descargo de responsabilidad: Este artículo y mis comentarios reflejan solo mi opinión y no tienen nada que ver con la posición oficial de InterSystems Corporation. Buen artículo, muy esclarecedor, bueno para entender un poco sobre cómo funciona y cómo usar global.
Artículo
Pierre-Yves Duquesnoy · 3 mayo, 2022

Depuración tradicional en ObjectScript

Encontrar errores en tu código o examinar un comportamiento inesperado es el principal objetivo de la depuración. Trataré de actualizar las herramientas tradicionales aparte de las ayudas que tienen Studio, VScode, Serenji... Las herramientas básicas que han estado ahí antes de que tu EDI preferido lo utilizara en segundo plano. Observamos 3 situaciones habituales: Depuración desde la línea de comandos en primer plano desde un terminal o una sesión similar. Si es posible, el antiguo comando BREAK (también con una postcondición) Te permite detenerte y examinar la situación cuando lo necesites. Es la opción más antigua, que se remonta al periodo anterior a ObjectScript. Más nuevo, pero similar, el comando ZBREAK, introduce puntos de control, además de simples Puntos de interrupción y un sofisticado conjunto de condiciones. ​​​ Aquí puedes consultar más información sobre la Depuración desde la línea de comandos Depuración en segundo plano Todo lo que se describe aquí es, por supuesto, igualmente útil y está disponible para la depuración desde la línea de comandos Primero, tienes la opción de escribir información adicional en un Global (temporal). Esto es útil si ya tienes una sospecha bastante clara de lo que hay que revisar Después, puedes volcar sus variables reales también a SPOOL o utilizar una salida redirigida o llamar a LOG^%ETN() para volcar toda la partición en ^ERRORS Global y examinarlo con SMP/System Operation/Application error log Como forma de mover el código de background al primer plano en la línea de comandos, puedes utilizar la depuración desde el intérprete de comandos o Shell. Esto se aplica normalmente a SQL y CSP/ZEN Depuración desde el intérprete de comandos Empiezo con una página CSP bastante simple como un ejemplo que contiene este código: . . . </head> <script language="Cache" method="init" arguments="file,.tName" returntype="%Integer"> BREAK ;; <<<< for debugging if 'file { set tName="*** no file ***",index="" } elseif $D(^ImportFile(file,-1)) { set tName=$O(^ImportFile(file,-1,"Class",""),1,index) } Else { set tName=$g(^ImportFile(file)),index=0 } quit index </script> <script language="Cache" method= Ahora ejecuto esto desde mi CSPshell y puedo ver y acceder a todas las variables y objetos IRISAPP> IRISAPP>do $system.CSP.Shell() CSP Shell CSP:IRISAPP>>> GET /csp/irisapp/Details.csp HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Set-Cookie: CSPSESSIONID-UP-csp-irisapp-=15340 15340; path=/csp/irisapp/; httpOnly; sameSite=strict; Cache-Control: no-cache Date: Tue, 23 Nov 2021 19:03:29 GMT Expires: Thu, 29 Oct 1998 17:04:19 GMT Pragma: no-cache <html> <head> <!-- Put your page Title here --> <title> Details </title> <script language="javascript"> window.onmessage = function(event){ }; </script> <!-- function postDebug(item, index) { var escape = item.replace(/\//g, '-'); // fix slashes in dates //var escape = encodeURI(escape); var escape = encodeURIComponent(escape); // fix % and more if (item == '') escape = 'NULL'; //document.getElementById("div_debug").innerHTML += index + "," + escape + "<br>"; var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { //document.getElementById("div_debug").innerHTML += this.responseText; } if (this.readyState == 4 && this.status != 200) { //document.getElementById("div_debug").innerHTML += this.responseText; } }; //xhttp.open("GET", "/restAll/debug", true, "_SYSTEM", "SYS"); //xhttp.open("POST", "/restAll/click/" + escape + '/' + index, true, "_SYSTEM", "abc123"); xhttp.open("POST", "/restAll/click/" + escape + '/' + index, true, "_SYSTEM", "SYS"); ;xhttp.send(); } </script --> </head> <body> <table> <BREAK>zinit+1^csp.details.1 ;<<<<<<<<<<<<<<<<<<<<<<<<<<< IRISAPP 15e1>zwrite %CSPsc=1 %SYSLOG=1 %request=<OBJECT REFERENCE>[4@%CSP.Request] %response=<OBJECT REFERENCE>[1@%CSP.Response] %session=<OBJECT REFERENCE>[2@%CSP.Session] file=0 tDEBUG=3 tData="class,dc.data.rcc.GMbadge" tFile1=0 tFile2=0 tWhat="class" IRISAPP 15e1>zwrite %request %request=4@%CSP.Request ; <OREF> +----------------- general information --------------- | oref value: 4 | class name: %CSP.Request | reference count: 5 +----------------- attribute values ------------------ | AppData = $lb("",64,1,"","/csp/irisapp/","",1,"","",0,1,"","","/csp/irisapp","IRISAPP","","c:\intersystems\iris\csp\irisapp\",1,"","",1,"",900,2,2,"",3600,0,1,1,"",1,"","",1,1,0,2,2) <Set> | AppMatch = "/csp/irisapp/" <Set> | Application = "/csp/irisapp/" <Set> | CSPGatewayRequest = 0 <Set> | CharSet = "" <Set> | Class = "csp.details" <Set> | ContentType = "" <Set> | (ConvertCharSet) = "UTF8" | GatewayApplication = "" <Set> | GatewayBuild = "" <Set> |GatewayConnectionName = "" <Set> | GatewayError = "" | GatewayFunctions = "" <Set> |GatewayInstanceName = "" <Set> | GatewayNewId = 0 |GatewaySessionCookie = "" <Set> |GatewaySessionIdSource = "" <Set> | GatewayTimeout = 60 | Method = "GET" | NoResetTimeout = 0 | PageName = "Details.csp" |ProcessedRequestType = 1 | Protocol = "HTTP/1.1" <Set> | RegistryMethods = 1 | RequestId = "" | Secure = 0 <Set> | Service = "CSP" <Set> | URL = "/csp/irisapp/Details.csp" | URLPrefix = "" | UserAgent = "" <Set> +----------------- swizzled references --------------- | i%Content = "" | r%Content = "" +----------------------------------------------------- IRISAPP 15e1>zwrite %session %session=2@%CSP.Session ; <OREF> +----------------- general information --------------- | oref value: 2 | class name: %CSP.Session | %%OID: $lb("15340","%CSP.Session") | reference count: 5 +----------------- attribute values ------------------ | %Concurrency = 1 <Set> | AppTimeout = 900 | Application = "/csp/irisapp/" <Set> |ApplicationLicenses = "" | BrowserName = "" <Get> | BrowserPlatform = "" <Get> | BrowserVersion = "" <Get> | ByIdGroups = "" | CSPSessionCookie = "15340 15340" | CookiePath = "/csp/irisapp/" | CreateTime = "2021-11-23 18:56:54" <Set> | Debug = 0 <Set> | EndSession = 0 <Set> | ErrorPage = "" |(EventClassContext) = "" | GetNewId = 0 | GroupId = "" | HttpAuthorization = "" | KeepAlive = 1 | Key = "3"_$c(3)_"±Ð~"_$c(129)_"­t¼Ø´0"_$c(0)_"òÆÆùXNX"_$c(158)_"Å="_$c(152,1)_"T*"_$c(10)_"®«®"_$c(21) <Set> | Language = "de" | LastModified = "2021-11-23 18:58:28" <Set> | LicenseId = "_SYSTEM" <Set> | LogoutCleanup = 0 | MessageNumber = 1 <Set> | Namespace = "IRISAPP" | NewSession = 0 <Set> | (NoLicense) = 0 | OldTimeout = 5708603608 <Set> | PersistentHeaders = "" <Set> | Preserve = 0 <Set> | ProcessId = "" | Referrer = "" <Set> | RunNamespace = "" <Get,Set> | SOAPRequestCount = 0 |SecureSessionCookie = 0 | SecurityContext = $lb("_SYSTEM","%All,%DB_IRISLIB,%DB_IRISSYS","%All,%DB_IRISLIB,%DB_IRISSYS",32,-559038737) <Set> | SessionId = 15340 | SessionScope = 2 | (StickyLogin) = $lb("/csp/irisapp/","_SYSTEM",$lb("_SYSTEM","%All,%DB_IRISLIB,%DB_IRISSYS","%All,%DB_IRISLIB,%DB_IRISSYS",32,-559038737),"",0,0,0,1) <Get,Set> | UseSessionCookie = 2 | UserAgent = "" | UserCookieScope = 2 | nosave = 0 +--------------- calculated references --------------- | EventClass <Get,Set> | Username <Get> +----------------------------------------------------- IRISAPP 15e1>zwrite %response %response=1@%CSP.Response ; <OREF> +----------------- general information --------------- | oref value: 1 | class name: %CSP.Response | reference count: 5 +----------------- attribute values ------------------ | AllowOutputFlush = 0 |AvoidPartitionCleanup = 0 | (CSPGatewayData) = "" <Set> | CharSet = "utf-8" | ContentLength = "" | ContentType = "text/html" | CookiePath = "/csp/irisapp/" | Domain = "" | GzipOutput = "" <Set> | HTTPVersion = "" | HeaderCharSet = "" |Headers("CACHE-CONTROL") = "no-cache" | Headers("DATE") = "Tue, 23 Nov 2021 19:03:29 GMT" | Headers("EXPIRES") = "Thu, 29 Oct 1998 17:04:19 GMT" | Headers("PRAGMA") = "no-cache" | IgnoreRESTOutput = "" | InProgress = 1 <Set> | Language = "de" <Set> | NoCharSetConvert = 0 | OutputSessionToken = 1 | Redirect = "" | ServerSideRedirect = "" | Status = "200 OK" | Timeout = "" <Set> | TraceDump = 0 | UseASPredirect = 0 <Set> | UseHttpOnly = 1 | VaryByParam = "" +--------------- calculated references --------------- | Expires <Get,Set> +----------------------------------------------------- IRISAPP 15e1>g <BREAK>zinit+1^csp.details.1 IRISAPP 15e1>g <!-- tr><th colspan="3">#(tName)#</th></tr --> <tr><th>Line</th><th>*** no file ***</th><th>Line</th><th>*** no file ***</th></tr> </table> </body> </html> El siguiente ejemplo se refiere a SQL y utilizo esta propiedad calculada que falla de vez en cuando Property Random As %Numeric(SCALE = 2) [ Calculated, SqlComputed, SqlComputeCode = { Set rcc=$Random(5) BREAK Set {*} = $Random(10)/rcc} ]; ---------------------------------------------------------------------------- SQLquery= "select top 10 id,name,random from Sample.Person" ---------------------------------------------------------------------------- throws [SQLCODE: <-350> . . . ] [%msg: <Unexpected error executing SqlCompute code for field 'Random': <DIVIDE>%0AmBk1+5^%sqlcq.DEMO.cls33.1>]</pre></li> es obvio que la depuración de %0AmBk1+5^%sqlcq.DEMO.cls33.1 está fuera de nuestro alcance Para la depuración yo utilizo el SQLshell para aprovechar mi BREAK para ver todas las variables y objetos DEMO> DEMO>do $system.SQL.Shell() SQL Command Line Shell ---------------------------------------------------- The command prefix is currently set to: <<nothing>>. Enter <command>, 'q' to quit, '?' for help. [SQL]DEMO>>select top 10 id,name,random from Sample.Person 2. select top 10 id,name,random from Sample.Person ID Name Random . try { Set rcc=$Random(5) BREAK Set i%RandomO1 = $Random(10)/rcc ^ <BREAK>%0AmBk1+5^%sqlcq.DEMO.cls33.1 DEMO 10d3>zw %SNGetQueryStats=1 SQLCODE=0 rcc=1 DEMO 10d3>zwrite $this 45@%sqlcq.DEMO.cls33 ; <OREF> +----------------- general information --------------- | oref value: 45 | class name: %sqlcq.DEMO.cls33 | reference count: 6 +----------------- attribute values ------------------ | %CurrentResult = "45@%sqlcq.DEMO.cls33" | %CursorNumber = 1 | %ExtendedMetadata = $lb($lb("Sample.Person||ID","%Library.BigInt",18),$lb("Sample.Person||Name","%Library.String",10),$lb("Sample.Person||Random","%Library.Numeric",14)) | %Message = "" | %Metadata(0) = $lb(3,"ID",-5,19,"0",1,"ID","Person","Sample",0,$c(0,0,0,1,0,0,0,0,0,0,0,0,0),"Name",12,50,"0",0,"Name","Person","Sample",0,$c(0,0,0,0,0,0,0,0,0,0,0,0,0),"Random",2,15,"2",1,"Random","Person","Sample",0,$c(0,0,0,0,0,0,0,0,0,0,0,0,0)) | (%NextColumn) = 1 | %Objects = "" | %OutputColumnCount = 0 | %Parameters = "" | %ROWCOUNT = 0 | %ROWID = "" | %ResultColumnCount = 3 | %SQLCODE = 0 | (%SelectMode) = 0 | %StatementType = 1 | (%delock11) = "" | %routine = "" | CursorState = 1 | (ID1) = 1 | (IDO1) = "" | (NameO1) = "Newton,Olga Z." | (PpCallArgs1) = 10 | (Vrowcnt17) = 0 | (isolationMode) = 0 | (lockstat11) = 0 | (node2val21) = $lb("",82732383,"OptiPlex Media Inc.",34350,"V968","P1169","OR","E8259","Z4197","","Newton,Olga Z.","U4734","P4","U8988","W9047","992-61-9877",-92713954810893571,"2018-07-16 17:44:34") | (rowcnt) = 0 | (rowlimit) = 9223372036854775807 | (starttime) = 9966.938434 | (time) = .000001 | (vpRUNTIMEOUT3) = "Newton,Olga Z." +----------------- swizzled references --------------- | i%%PrivateTables = "" <Set> | r%%PrivateTables = "" <Set> | (i%%ProcCursor) = "" | (r%%ProcCursor) = "" | (i%%rsmd) = "" | (r%%rsmd) = "50@%SQL.StatementMetadata" +--------------- calculated references --------------- | %StatementTypeName <Get> | ID <Get> [ Aliases - id ] | Name <Get> [ Aliases - name,NAME ] | Random <Get> [ Aliases - random,RANDOM ] +----------------------------------------------------- DEMO 10d3>g Newton,Olga Z. 1 . try { Set rcc=$Random(5) BREAK Set i%RandomO1 = $Random(10)/rcc ^ <BREAK>%0AmBk1+5^%sqlcq.DEMO.cls33.1 DEMO 10d3>zwrite %SNGetQueryStats=1 SQLCODE=0 rcc=1 DEMO 10d3>g Adam,Emily G. 7 . try { Set rcc=$Random(5) BREAK Set i%RandomO1 = $Random(10)/rcc ^ <BREAK>%0AmBk1+5^%sqlcq.DEMO.cls33.1 DEMO 10d3>zw %SNGetQueryStats=1 SQLCODE=0 rcc=0 DEMO 10d3>g ; now we get the error [SQLCODE: <-350>] [%msg: <Unexpected error executing SqlCompute code for field 'Random': <DIVIDE>%0AmBk1+5^%sqlcq.DEMO.cls33.1>] 2 Rows(s) Affected statement prepare time(s)/globals/cmds/disk: 0.0003s/4/137/0ms execute time(s)/globals/cmds/disk: 44.5198s/37/839/0ms cached query class: %sqlcq.DEMO.cls33 --------------------------------------------------------------------------- [SQL]DEMO>>q DEMO> Y, como puedes ver, estos métodos de depuración actualmente también funcionan en IRIS. Esta es una práctica personal recomendada por Robert Cemper.
Artículo
Ricardo Paiva · 24 feb, 2023

Depuración web - Parte 2

En el [primer artículo](https://es.community.intersystems.com/post/depuraci%C3%B3n-web) hablé sobre probar y depurar aplicaciones web de Caché con herramientas externas. La segunda parte tratará sobre las herramientas de Caché. Estas son: * CSP Gateway y Webapp configuration * CSP Gateway logging * CSP Gateway tracing * ISCLOG * Custom logging * Session events * Output to device ### CSP Gateway y Webapp configuration En primer lugar, si estás depurando y, sobre todo, desarrollando una aplicación front-end, no necesitas *caching*. Es muy útil en un sistema de producción, pero no durante el desarrollo. Para desactivar el registro de una aplicación web, hay que ir a: SMP → Menu → Manage Web Applications → y definir Serve Files Timeout con la configuración igual a 0. Luego hacer clic en "Save". Después, hay que purgar el caché de la aplicación web. Para ello, ve a: SMP → System Administration → Configuration → CSP Gateway Management → System Status. Allí se encuentra la tabla "Cached Forms", la última fila es una línea Total, pulsa el botón borrar (con el punto) para borrar el caché de la aplicación web: ![](/sites/default/files/inline/images/snimok_26.png) ### CSP Gateway logging Ya que estamos hablando de CSP Gateway, este es compatible con el registro de las solicitudes de entrada (documentación). En la pestaña Default Parameters, especifica el nivel de registro deseado (por ejemplo, v9a) y guarda los cambios. v9a (consulta otras opciones en la documentación) registra todas las solicitudes HTTP a http.log en el directorio principal de Gateway. Este es un ejemplo de solicitud capturada: GET /forms/form/info/Form.Test.Person HTTP/1.1 Host: localhost:57772 User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0 Accept: application/json, text/plain, */* Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Referer: http://localhost:57772/formsui/index.html Cookie: CSPSESSIONID-SP-57772-UP-forms-=001000000000yxiocLLb8bbc9SVXQJC5WMU831n2sENf4OGeGa; CSPWSERVERID=144zwBTP Dnt: 1 Connection: keep-alive Cache-Control: max-age=0 <> También hay opciones para registrar el rendimiento. Los resultados pueden escribirse en un archivo o visualizarse desde la pestaña View Event Log. ### CSP Gateway tracing Por último, se pueden rastrean las solicitudes y respuestas en la pestaña View HTTP Trace de CSP Gateway. Activa el rastreo y las solicitudes comenzarán a ser capturadas inmediatamente. No olvides apagarlo después de realizar la depuración. Este es un ejemplo de sesión de depuración: ![](/sites/default/files/inline/images/snimok_27.png) Nota: utiliza el rastreo si puedes identificar cuál es el problema y reproducirlo fácilmente. Utiliza el registro para recoger estadísticas, elaborar perfiles de rendimiento, etc. Además, la mayoría de los servidores web ofrecen herramientas de registro y seguimiento del rendimiento. ### ISCLOG CSP Gateway es útil para determinar problemas de red y hacer seguimiento del rendimiento, pero para registrar lo que ocurre dentro de Caché se necesitan otras herramientas. Una de las herramientas más versátiles es ISCLOG. [Documentación](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GCSP_logging). Es un *global* que puede almacenar información sobre el procesamiento de las solicitudes actuales. Para iniciar el registro, ejecuta: set ^%ISCLOG = 2 Y para terminar el registro, ejecuta: set ^%ISCLOG = 0 Este es el ejemplo de una solicitud: ^%ISCLOG=0 ^%ISCLOG("Data")=24 ^%ISCLOG("Data",1)=$lb(2,"CSPServer","Header from CSP Size:3744 CMD:h IdSource:3","4664","FORMS","2017-06-07 10:49:21.341","%SYS.cspServer2","","") ^%ISCLOG("Data",1,0)="^h30000 "_$c(14,0,0)_"A" ^%ISCLOG("Data",2)=$lb(2,"CSPServer","[UpdateURL] Looking up: //localhost/forms/form/info path found: //localhost/forms/ Appl= "_$c(2,1,3,4)_"@"_$c(3,4,1,2,1,9,1)_"/forms/"_$c(2,1,3,4,1,2,1,2,1,2,4,3,4,1,2,1,9,1,7,1)_":%All"_$c(8,1)_"/forms"_$c(7,1)_"FORMS"_$c(2,1,2,1,3,4,1,2,1,2,1,3,4,1,2,1,4,4,132,3,3,4,2,3,4,2,2,1,4,4,16,14,2,4,3,4,1,3,4,1,2,1,3,4,1,2,1,16,1)_"Form.REST.Main"_$c(2,4,2,4),"4664","FORMS","2017-06-07 10:49:21.342","%CSP.Request.1","124","L3DfNILTaE") ^%ISCLOG("Data",3)=$lb(2,"CSPServer","[UpdateURL] Found cls: Form.REST.Main nocharsetconvert:  charset:UTF-8 convert charset:UTF8","4664","FORMS","2017-06-07 10:49:21.342","%CSP.Request.1","124","L3DfNILTaE") ^%ISCLOG("Data",4)=$lb(2,"CSPServer","[HTML] Determined request type","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer2","124","L3DfNILTaE") ^%ISCLOG("Data",4,0)=$lb("usesession",1,"i%Class","Form.REST.Main","i%Service","REST","NOLOCKITEM","","i%GatewayError","") ^%ISCLOG("Data",5)=$lb(2,"CSPSession","[%LoadData] Loading CSP session, nosave=0","4664","FORMS","2017-06-07 10:49:21.342","%CSP.Session.1","","L3DfNILTaE") ^%ISCLOG("Data",5,0)=$lb(900,,0,5567742244,$c(149)_"Ù"_$c(3)_"ó»à"_$c(127)_",½"_$c(149,10)_"\"_$c(18)_"v"_$c(128,135)_"3Vô"_$c(11)_"*"_$c(154)_"PÏG¥"_$c(140,157,145,10,131)_"*",2,"FORMS","001000000000L3DfNILTaE1cDBJNjyQdyLwKq4wCXP82ld8gic",,0,"ru","L3DfNILTaE",2,1,"/forms/",$lb("UnknownUser","%All,%Developer","%All,%Developer",64,-559038737),"","","","2017-06-07 10:48:51","2017-06-07 10:49:04","Basic ZGV2OjEyMw==","Mozilla/5.0 (Windows NT 10.0; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0","","",0,"/forms/","","","",4,"","","","","http://localhost:57772/formsui/index.html") ^%ISCLOG("Data",6)=$lb(2,"CSPServer","[CSPDispatch]Requested GET /forms/form/info","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer","124","L3DfNILTaE") ^%ISCLOG("Data",7)=$lb(2,"CSPServer","[CSPDispatch] ** Start processing request newSes=0","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer","124","L3DfNILTaE") ^%ISCLOG("Data",7,0)="/forms/form/info" ^%ISCLOG("Data",8)=$lb(2,"CSPServer","[CSPDispatch] Service type is REST has-soapaction=0 nosave=0","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer","124","L3DfNILTaE") ^%ISCLOG("Data",9)=$lb(2,"CSPServer","[CSPDispatch]About to run page: Form.REST.Main","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer","124","L3DfNILTaE") ^%ISCLOG("Data",9,0)=$lb("UnknownUser","%All,%Developer","%All,%Developer",64,-559038737) ^%ISCLOG("Data",10)=$lb(2,"CSPServer","[callPage] url=/forms/form/info ; Appl: /forms/ newsession=0","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer","124","L3DfNILTaE") ^%ISCLOG("Data",11)=$lb(2,"CSPServer","[callPage]Imported security context ; User: UnknownUser ; Roles: %All,%Developer","4664","FORMS","2017-06-07 10:49:21.342","%SYS.cspServer","124","L3DfNILTaE") ^%ISCLOG("Data",12)=$lb(2,"CSPServer","[OutputCSPGatewayData]: chd=1;","4664","FORMS","2017-06-07 10:49:21.431","%CSP.Response.1","","L3DfNILTaE") ^%ISCLOG("Data",13)=$lb(2,"CSPResponse","[WriteHTTPHeaderCookies] Session cookie: CSPSESSIONID-SP-57772-UP-forms-=001000000000L3DfNILTaE1cDBJNjyQdyLwKq4wCXP82ld8gic; path=/forms/;  httpOnly;","4664","FORMS","2017-06-07 10:49:21.431","%CSP.Response.1","124","L3DfNILTaE") ^%ISCLOG("Data",14)=$lb(2,"CSPServer","[callPage] Return Status","4664","FORMS","2017-06-07 10:49:21.431","%SYS.cspServer","124","L3DfNILTaE") ^%ISCLOG("Data",14,0)=1 ^%ISCLOG("Data",15)=$lb(2,"CSPServer","[OutputCSPGatewayData]: chd=1;","4664","FORMS","2017-06-07 10:49:21.431","%CSP.Response.1","","L3DfNILTaE") ^%ISCLOG("Data",16)=$lb(2,"CSPServer","[Cleanup]Page EndSession=0; needToGetALicense=-1; nosave=0; loginredirect=0; sessionContext=1","4664","FORMS","2017-06-07 10:49:21.431","%SYS.cspServer","124","L3DfNILTaE") ^%ISCLOG("Data",17)=$lb(2,"CSPSession","[Cleanup] EndSession=0 nosave=0","4664","FORMS","2017-06-07 10:49:21.431","%SYS.cspServer","124","L3DfNILTaE") ^%ISCLOG("Data",18)=$lb(2,"CSPSession","[%SaveData] Saved: ","4664","FORMS","2017-06-07 10:49:21.431","%CSP.Session.1","","L3DfNILTaE") ^%ISCLOG("Data",18,0)=$lb(900,,0,5567742261,$c(149)_"Ù"_$c(3)_"ó»à"_$c(127)_",½"_$c(149,10)_"\"_$c(18)_"v"_$c(128,135)_"3Vô"_$c(11)_"*"_$c(154)_"PÏG¥"_$c(140,157,145,10,131)_"*",2,"FORMS","001000000000L3DfNILTaE1cDBJNjyQdyLwKq4wCXP82ld8gic",,0,"ru","L3DfNILTaE",2,1,"/forms/",$lb("UnknownUser","%All,%Developer","%All,%Developer",64,-559038737),"","","","2017-06-07 10:48:51","2017-06-07 10:49:21","Basic ZGV2OjEyMw==","Mozilla/5.0 (Windows NT 10.0; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0","","",0,"/forms/","","","",5,"","","","","http://localhost:57772/formsui/index.html") ^%ISCLOG("Data",19)=$lb(2,"CSPServer","[Cleanup] Restoring roles before running destructor","4664","FORMS","2017-06-07 10:49:21.431","%SYS.cspServer","","L3DfNILTaE") ^%ISCLOG("Data",19,0)=$lb("UnknownUser","%All,%Developer","%All,%Developer",64,-559038737) ^%ISCLOG("Data",20)=$lb(2,"CSPServer","[Cleanup] End","4664","FORMS","2017-06-07 10:49:21.431","%SYS.cspServer","","L3DfNILTaE") ^%ISCLOG("Data",20,0)="<-Finish processing request->" ^%ISCLOG("Data",21)=$lb(2,"GatewayRequest","[CSPGWClientRequest] GWID: ed-pc:57772; Request: sys_get_system_metricsTimeout: 5","11112","%SYS","2017-06-07 10:49:23.141","%SYS.cspServer3","","") ^%ISCLOG("Data",22)=$lb(2,"GatewayRequest","[CSPGWClientRequest] GWID: 127.0.0.1:57772; Request: sys_get_system_metricsTimeout: 5","11112","%SYS","2017-06-07 10:49:23.141","%SYS.cspServer3","","") ^%ISCLOG("Data",23)=$lb(2,"GatewayRequest","[SendSimpleCmd:Server:Failed] WebServer: 127.0.0.1:57772; Gateway Server Request Failed","11112","%SYS","2017-06-07 10:49:23.141","%CSP.Mgr.GatewayMgrImpl.1","","") ^%ISCLOG("Data",23,0)=0 ^%ISCLOG("Data",24)=$lb(2,"GatewayRequest","[GetMetrics]","11112","%SYS","2017-06-07 10:49:23.141","%CSP.Mgr.GatewayMgrImpl.1","","") ^%ISCLOG("Data",24,0)="<-End Request Client->" Además, este es un script rápido para la salida de *global* a un archivo: set p="c:\temp\isclog.txt" open p:"NW" use p zw ^%ISCLOG close p   ### Custom logging Aunque las herramientas de registro predeterminadas son bastante buenas, tienen varios problemas: * Son genéricas y no están familiarizadas con tu solicitud * Las opciones más detalladas afectan al rendimiento * No están bien estructuradas, por lo que puede resultar difícil extraer información Por lo tanto, se pueden cubrir casos más específicos escribiendo sistemas de registro personalizados. Este es un ejemplo de una clase persistente que registra parte del objeto %request: /// Incoming request Class Log.Request Extends %Persistent { /// A string indicating HTTP method used for this request. Property method As %String; /// A string containing the URL up to and including the page name /// and extension, but not including the query string. Property url As %String(MAXLEN = ""); /// A string indicating the type of browser from which the request /// originated, as determined from the HTTP_USER_AGENT header. Property userAgent As %String(MAXLEN = ""); /// A string indicating the MIME Content-Type of the request. Property contentType As %String(MAXLEN = ""); /// Character set this request was send in, if not specified in the HTTP headers /// it defaults to the character set of the page it is being submitted to. Property charSet As %String(MAXLEN = ""); /// A <class>%CSP.Stream</class> containing the content submitted /// with this request. Property content As %Stream.GlobalBinary; /// True if the communication between the browser and the web server was using /// the secure https protocol. False for a normal http connection. Property secure As %Boolean; Property cgiEnvs As array Of %String(MAXLEN = "", SQLPROJECTION = "table/column"); Property data As array Of %String(MAXLEN = "", SQLPROJECTION = "table/column"); ClassMethod add() As %Status { set request = ..%New() quit request.%Save() } Method %OnNew() As %Status [ Private, ServerOnly = 1 ] { #dim %request As %CSP.Request #dim sc As %Status = $$$OK quit:'$isObject($g(%request)) $$$ERROR($$$GeneralError, "Not a web context") set ..charSet = %request.CharSet if $isObject(%request.Content) { do ..content.CopyFromAndSave(%request.Content) } else { set ..content = "" } set ..contentType = %request.ContentType set ..method = %request.Method set ..secure = %request.Secure set ..url = %request.URL set ..userAgent = %request.UserAgent set cgi = "" for { set cgi=$order(%request.CgiEnvs(cgi)) quit:cgi="" do ..cgiEnvs.SetAt(%request.CgiEnvs(cgi), cgi) } // Only gets first data if more than one data with the same name is present set data = "" for { set data=$order(%request.Data(data)) quit:data="" do ..data.SetAt(%request.Get(data), data) } quit sc } } Para añadir un nuevo registro a la tabla Log.Request, agrega una llamada a tu código: do ##class(Log.Request).add() Es un ejemplo muy básico, puede y debe ampliarse para registrar comentarios, variables o cualquier otra cosa que puedas necesitar. La principal ventaja de este enfoque es la posibilidad de ejecutar consultas SQL sobre los datos registrados.  Para obtener más información sobre cómo crear tu propio sistema de registro, consulta [este artículo](https://es.community.intersystems.com/post/logging-usando-macros-en-intersystems-cach%C3%A9). ### Session events La clase Event (Evento) es una clase que define las interfaces que se llaman durante la vida de un objeto [%CSP.Session](http://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25CSP.Session). Para utilizarla, hay que subclasificar [%CSP.SessionEvents](http://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25CSP.SessionEvents) e implementar el código del método que quieres ejecutar. A continuación, en la configuración de la aplicación CSP, hay que establecer la clase de evento en la clase que has creado. Las siguientes devoluciones de llamada están disponibles: OnApplicationChange OnEndRequest OnEndSession OnLogin OnLogout OnStartRequest OnStartSession * OnTimeout Por ejemplo, los custom loggings comentado anteriormente pueden invocarse desde estos métodos. ### Output to device Una de las opciones más sencillas - un método de utilidad CSP que muestra todos los objetos como respuesta. Solo hay que añadir esto a cualquier parte de tu código: set %response.ContentType = "html" do ##class(%CSP.Utils).DisplayAllObjects() return $$$OK   ### Conclusión Hay varias herramientas que se puede utilizar para depurar las aplicaciones web. Elige la más adecuada para la tarea que vayas a realizar.   Y vosotros... ¿tenéis algún consejo o truco para depurar aplicaciones web desde Caché?